diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1077230812b60..0bdddddab8de5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,6 +7,7 @@ /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app +/src/plugins/charts/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app @@ -59,7 +60,6 @@ # APM /x-pack/plugins/apm/ @elastic/apm-ui /x-pack/test/functional/apps/apm/ @elastic/apm-ui -/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @watson @vigneshshanmugam @@ -83,9 +83,6 @@ /src/plugins/home/public @elastic/kibana-core-ui /src/plugins/home/server/*.ts @elastic/kibana-core-ui /src/plugins/home/server/services/ @elastic/kibana-core-ui -# Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon -/src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui -/src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui # Observability UIs @@ -167,7 +164,6 @@ # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform -/x-pack/legacy/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security @@ -285,8 +281,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Core design /src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers -/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers -/x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml index 039b520561d65..2e8529b4a7704 100644 --- a/.github/paths-labeller.yml +++ b/.github/paths-labeller.yml @@ -16,3 +16,11 @@ - "x-pack/test/epm_api_integration/**/*.*" - "Team:uptime": - "x-pack/plugins/uptime/**/*.*" + - "x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/*.*" + - "x-pack/plugins/apm/public/application/csmApp.tsx" + - "x-pack/plugins/apm/public/components/app/RumDashboard/**/*.*" + - "x-pack/plugins/apm/public/components/app/RumDashboard/*.*" + - "x-pack/plugins/apm/server/lib/rum_client/**/*.*" + - "x-pack/plugins/apm/server/lib/rum_client/*.*" + - "x-pack/plugins/apm/server/routes/rum_client.ts" + - "x-pack/plugins/apm/server/projections/rum_overview.ts" diff --git a/docs/developer/architecture/security/feature-registration.asciidoc b/docs/developer/architecture/security/feature-registration.asciidoc index 3ff83e9db8c43..b27e457940d93 100644 --- a/docs/developer/architecture/security/feature-registration.asciidoc +++ b/docs/developer/architecture/security/feature-registration.asciidoc @@ -38,6 +38,12 @@ Registering a feature consists of the following fields. For more information, co |`"Sample Feature"` |A human readable name for your feature. +|`category` (required) +|{kib-repo}blob/{branch}/src/core/types/app_category.ts[`AppCategory`] +|`DEFAULT_APP_CATEGORIES.kibana` +|The `AppCategory` which best represents your feature. Used to organize the display +of features within the management screens. + |`app` (required) |`string[]` |`["sample_app", "kibana"]` @@ -96,6 +102,7 @@ public setup(core, { features }) { name: 'Canvas', icon: 'canvasApp', navLinkId: 'canvas', + category: DEFAULT_APP_CATEGORIES.kibana, app: ['canvas', 'kibana'], catalogue: ['canvas'], privileges: { @@ -155,6 +162,7 @@ public setup(core, { features }) { }), icon: 'devToolsApp', navLinkId: 'dev_tools', + category: DEFAULT_APP_CATEGORIES.management, app: ['kibana'], catalogue: ['console', 'searchprofiler', 'grokdebugger'], privileges: { @@ -217,6 +225,7 @@ public setup(core, { features }) { order: 100, icon: 'discoverApp', navLinkId: 'discover', + category: DEFAULT_APP_CATEGORIES.kibana, app: ['kibana'], catalogue: ['discover'], privileges: { diff --git a/docs/development/core/server/kibana-plugin-core-server.appcategory.arialabel.md b/docs/development/core/server/kibana-plugin-core-server.appcategory.arialabel.md new file mode 100644 index 0000000000000..fe81f7cffaa41 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.appcategory.arialabel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AppCategory](./kibana-plugin-core-server.appcategory.md) > [ariaLabel](./kibana-plugin-core-server.appcategory.arialabel.md) + +## AppCategory.ariaLabel property + +If the visual label isn't appropriate for screen readers, can override it here + +Signature: + +```typescript +ariaLabel?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.appcategory.euiicontype.md b/docs/development/core/server/kibana-plugin-core-server.appcategory.euiicontype.md new file mode 100644 index 0000000000000..79de37ea619f3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.appcategory.euiicontype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AppCategory](./kibana-plugin-core-server.appcategory.md) > [euiIconType](./kibana-plugin-core-server.appcategory.euiicontype.md) + +## AppCategory.euiIconType property + +Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined + +Signature: + +```typescript +euiIconType?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.appcategory.id.md b/docs/development/core/server/kibana-plugin-core-server.appcategory.id.md new file mode 100644 index 0000000000000..f0889d200725a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.appcategory.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AppCategory](./kibana-plugin-core-server.appcategory.md) > [id](./kibana-plugin-core-server.appcategory.id.md) + +## AppCategory.id property + +Unique identifier for the categories + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.appcategory.label.md b/docs/development/core/server/kibana-plugin-core-server.appcategory.label.md new file mode 100644 index 0000000000000..9405118ed7a11 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.appcategory.label.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AppCategory](./kibana-plugin-core-server.appcategory.md) > [label](./kibana-plugin-core-server.appcategory.label.md) + +## AppCategory.label property + +Label used for category name. Also used as aria-label if one isn't set. + +Signature: + +```typescript +label: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.appcategory.md b/docs/development/core/server/kibana-plugin-core-server.appcategory.md new file mode 100644 index 0000000000000..a761bf4e5b393 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.appcategory.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AppCategory](./kibana-plugin-core-server.appcategory.md) + +## AppCategory interface + +A category definition for nav links to know where to sort them in the left hand nav + +Signature: + +```typescript +export interface AppCategory +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [ariaLabel](./kibana-plugin-core-server.appcategory.arialabel.md) | string | If the visual label isn't appropriate for screen readers, can override it here | +| [euiIconType](./kibana-plugin-core-server.appcategory.euiicontype.md) | string | Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined | +| [id](./kibana-plugin-core-server.appcategory.id.md) | string | Unique identifier for the categories | +| [label](./kibana-plugin-core-server.appcategory.label.md) | string | Label used for category name. Also used as aria-label if one isn't set. | +| [order](./kibana-plugin-core-server.appcategory.order.md) | number | The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) | + diff --git a/docs/development/core/server/kibana-plugin-core-server.appcategory.order.md b/docs/development/core/server/kibana-plugin-core-server.appcategory.order.md new file mode 100644 index 0000000000000..aba1b886076ad --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.appcategory.order.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AppCategory](./kibana-plugin-core-server.appcategory.md) > [order](./kibana-plugin-core-server.appcategory.order.md) + +## AppCategory.order property + +The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) + +Signature: + +```typescript +order?: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index b83c091846f04..be8b7c27495ad 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -50,6 +50,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Interface | Description | | --- | --- | +| [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) | | | [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) | Event to audit. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index bc34d4113f847..4422b755faa77 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -35,7 +35,7 @@ esFilters: { type?: string | undefined; key?: string | undefined; params?: any; - value?: string | ((formatter?: import("../common").FilterValueFormatter | undefined) => string) | undefined; + value?: string | undefined; }; $state?: import("../common").FilterState | undefined; query?: any; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter._state.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter._state.md deleted file mode 100644 index bfb5dff71e70d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter._state.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Filter](./kibana-plugin-plugins-data-public.filter.md) > [$state](./kibana-plugin-plugins-data-public.filter._state.md) - -## Filter.$state property - -Signature: - -```typescript -$state?: FilterState; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.md index f993721ee96ad..9212b757e07df 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.md @@ -2,19 +2,14 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Filter](./kibana-plugin-plugins-data-public.filter.md) -## Filter interface +## Filter type Signature: ```typescript -export interface Filter +export declare type Filter = { + $state?: FilterState; + meta: FilterMeta; + query?: any; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [$state](./kibana-plugin-plugins-data-public.filter._state.md) | FilterState | | -| [meta](./kibana-plugin-plugins-data-public.filter.meta.md) | FilterMeta | | -| [query](./kibana-plugin-plugins-data-public.filter.query.md) | any | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.meta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.meta.md deleted file mode 100644 index 3385a3773a2aa..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.meta.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Filter](./kibana-plugin-plugins-data-public.filter.md) > [meta](./kibana-plugin-plugins-data-public.filter.meta.md) - -## Filter.meta property - -Signature: - -```typescript -meta: FilterMeta; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.query.md deleted file mode 100644 index 083b544493e80..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filter.query.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Filter](./kibana-plugin-plugins-data-public.filter.md) > [query](./kibana-plugin-plugins-data-public.filter.query.md) - -## Filter.query property - -Signature: - -```typescript -query?: any; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.language.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.language.md deleted file mode 100644 index 127ee9210799e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.language.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Query](./kibana-plugin-plugins-data-public.query.md) > [language](./kibana-plugin-plugins-data-public.query.language.md) - -## Query.language property - -Signature: - -```typescript -language: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.md index a1dffe5ff5fa4..e15b04236a0b5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.md @@ -2,18 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Query](./kibana-plugin-plugins-data-public.query.md) -## Query interface +## Query type Signature: ```typescript -export interface Query +export declare type Query = { + query: string | { + [key: string]: any; + }; + language: string; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [language](./kibana-plugin-plugins-data-public.query.language.md) | string | | -| [query](./kibana-plugin-plugins-data-public.query.query.md) | string | {
[key: string]: any;
} | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.query.md deleted file mode 100644 index 9fcd0310af0fe..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.query.query.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Query](./kibana-plugin-plugins-data-public.query.md) > [query](./kibana-plugin-plugins-data-public.query.query.md) - -## Query.query property - -Signature: - -```typescript -query: string | { - [key: string]: any; - }; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.from.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.from.md deleted file mode 100644 index b428bd9cd90ca..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.from.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) > [from](./kibana-plugin-plugins-data-public.timerange.from.md) - -## TimeRange.from property - -Signature: - -```typescript -from: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.md index 69078ca40d20d..482501e494c7a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.md @@ -2,19 +2,14 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) -## TimeRange interface +## TimeRange type Signature: ```typescript -export interface TimeRange +export declare type TimeRange = { + from: string; + to: string; + mode?: 'absolute' | 'relative'; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [from](./kibana-plugin-plugins-data-public.timerange.from.md) | string | | -| [mode](./kibana-plugin-plugins-data-public.timerange.mode.md) | 'absolute' | 'relative' | | -| [to](./kibana-plugin-plugins-data-public.timerange.to.md) | string | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.mode.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.mode.md deleted file mode 100644 index fb9ebd3c9165f..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.mode.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) > [mode](./kibana-plugin-plugins-data-public.timerange.mode.md) - -## TimeRange.mode property - -Signature: - -```typescript -mode?: 'absolute' | 'relative'; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.to.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.to.md deleted file mode 100644 index 342acd5e049f1..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timerange.to.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) > [to](./kibana-plugin-plugins-data-public.timerange.to.md) - -## TimeRange.to property - -Signature: - -```typescript -to: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter._state.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter._state.md deleted file mode 100644 index 079f352609a70..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter._state.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [Filter](./kibana-plugin-plugins-data-server.filter.md) > [$state](./kibana-plugin-plugins-data-server.filter._state.md) - -## Filter.$state property - -Signature: - -```typescript -$state?: FilterState; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.md index 4e4c49b222f01..519bbaf8f9416 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.md @@ -2,19 +2,14 @@ [Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [Filter](./kibana-plugin-plugins-data-server.filter.md) -## Filter interface +## Filter type Signature: ```typescript -export interface Filter +export declare type Filter = { + $state?: FilterState; + meta: FilterMeta; + query?: any; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [$state](./kibana-plugin-plugins-data-server.filter._state.md) | FilterState | | -| [meta](./kibana-plugin-plugins-data-server.filter.meta.md) | FilterMeta | | -| [query](./kibana-plugin-plugins-data-server.filter.query.md) | any | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.meta.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.meta.md deleted file mode 100644 index 6d11804704d82..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.meta.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [Filter](./kibana-plugin-plugins-data-server.filter.md) > [meta](./kibana-plugin-plugins-data-server.filter.meta.md) - -## Filter.meta property - -Signature: - -```typescript -meta: FilterMeta; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.query.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.query.md deleted file mode 100644 index 942c7930f449d..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.filter.query.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [Filter](./kibana-plugin-plugins-data-server.filter.md) > [query](./kibana-plugin-plugins-data-server.filter.query.md) - -## Filter.query property - -Signature: - -```typescript -query?: any; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index f5b587d86b349..3c477e17503f4 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -42,7 +42,6 @@ | [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) | | | [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-server.fieldformatconfig.md) | | -| [Filter](./kibana-plugin-plugins-data-server.filter.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-server.iessearchrequest.md) | | | [IEsSearchResponse](./kibana-plugin-plugins-data-server.iessearchresponse.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | | @@ -58,12 +57,10 @@ | [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) | | | [PluginSetup](./kibana-plugin-plugins-data-server.pluginsetup.md) | | | [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) | | -| [Query](./kibana-plugin-plugins-data-server.query.md) | | | [RefreshInterval](./kibana-plugin-plugins-data-server.refreshinterval.md) | | | [SearchUsage](./kibana-plugin-plugins-data-server.searchusage.md) | | | [TabbedAggColumn](./kibana-plugin-plugins-data-server.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-server.tabbedtable.md) | \* | -| [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | | ## Variables @@ -91,11 +88,14 @@ | [AggParam](./kibana-plugin-plugins-data-server.aggparam.md) | | | [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-server.esaggsexpressionfunctiondefinition.md) | | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-server.fieldformatsgetconfigfn.md) | | +| [Filter](./kibana-plugin-plugins-data-server.filter.md) | | | [IAggConfig](./kibana-plugin-plugins-data-server.iaggconfig.md) | AggConfig This class represents an aggregation, which is displayed in the left-hand nav of the Visualize app. | | [IAggType](./kibana-plugin-plugins-data-server.iaggtype.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-server.ifieldformatsregistry.md) | | | [IFieldParamType](./kibana-plugin-plugins-data-server.ifieldparamtype.md) | | | [IMetricAggType](./kibana-plugin-plugins-data-server.imetricaggtype.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-server.parsedinterval.md) | | +| [Query](./kibana-plugin-plugins-data-server.query.md) | | | [TabbedAggRow](./kibana-plugin-plugins-data-server.tabbedaggrow.md) | \* | +| [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.language.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.language.md deleted file mode 100644 index 384fc77d801c0..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.language.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [Query](./kibana-plugin-plugins-data-server.query.md) > [language](./kibana-plugin-plugins-data-server.query.language.md) - -## Query.language property - -Signature: - -```typescript -language: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.md index 5d61c75bc5e99..6a7bdfe51f1c0 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.md @@ -2,18 +2,15 @@ [Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [Query](./kibana-plugin-plugins-data-server.query.md) -## Query interface +## Query type Signature: ```typescript -export interface Query +export declare type Query = { + query: string | { + [key: string]: any; + }; + language: string; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [language](./kibana-plugin-plugins-data-server.query.language.md) | string | | -| [query](./kibana-plugin-plugins-data-server.query.query.md) | string | {
[key: string]: any;
} | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.query.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.query.md deleted file mode 100644 index 5c2aa700bc603..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.query.query.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [Query](./kibana-plugin-plugins-data-server.query.md) > [query](./kibana-plugin-plugins-data-server.query.query.md) - -## Query.query property - -Signature: - -```typescript -query: string | { - [key: string]: any; - }; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.from.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.from.md deleted file mode 100644 index b6f40cc2e4203..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.from.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) > [from](./kibana-plugin-plugins-data-server.timerange.from.md) - -## TimeRange.from property - -Signature: - -```typescript -from: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.md index 8280d924eb609..1ac59343220fd 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.md @@ -2,19 +2,14 @@ [Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) -## TimeRange interface +## TimeRange type Signature: ```typescript -export interface TimeRange +export declare type TimeRange = { + from: string; + to: string; + mode?: 'absolute' | 'relative'; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [from](./kibana-plugin-plugins-data-server.timerange.from.md) | string | | -| [mode](./kibana-plugin-plugins-data-server.timerange.mode.md) | 'absolute' | 'relative' | | -| [to](./kibana-plugin-plugins-data-server.timerange.to.md) | string | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.mode.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.mode.md deleted file mode 100644 index 1408fb43cbf39..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.mode.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) > [mode](./kibana-plugin-plugins-data-server.timerange.mode.md) - -## TimeRange.mode property - -Signature: - -```typescript -mode?: 'absolute' | 'relative'; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.to.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.to.md deleted file mode 100644 index 98aca5474d350..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.timerange.to.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [TimeRange](./kibana-plugin-plugins-data-server.timerange.md) > [to](./kibana-plugin-plugins-data-server.timerange.to.md) - -## TimeRange.to property - -Signature: - -```typescript -to: string; -``` diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index 85230f1b6f70d..e3d0e16630c5c 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -1,3 +1,4 @@ +[role="xpack"] [[drilldowns]] == Use drilldowns for dashboard actions diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index 4919625340da2..e6daf89d72718 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -1,6 +1,8 @@ [[url-drilldown]] === URL drilldown +beta[] + The URL drilldown allows you to navigate from a dashboard to an internal or external URL. The destination URL can be dynamic, depending on the dashboard context or user’s interaction with a visualization. diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index 47fbe1bea9f2a..9d735ea1fe3db 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -8,9 +8,20 @@ If you enable the Elastic {monitor-features} in your cluster, you can optionally collect metrics about {kib}. +[IMPORTANT] +========================= +{metricbeat} is the recommended method for collecting and shipping monitoring +data to a monitoring cluster. + +If you have previously configured legacy collection methods, you should migrate +to using {metricbeat} collection methods. Use either {metricbeat} collection or +legacy collection methods; do not use both. + +For the recommended method, refer to <>. +========================= + The following method involves sending the metrics to the production cluster, -which ultimately routes them to the monitoring cluster. For the recommended -method, see <>. +which ultimately routes them to the monitoring cluster. To learn about monitoring in general, see {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index 8e246960937ec..4141b48ffeeaf 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -19,6 +19,7 @@ import { Plugin, CoreSetup } from 'kibana/server'; import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '../../../src/core/server'; import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plugins/features/server'; @@ -47,6 +48,7 @@ export class AlertingExamplePlugin implements Plugin', diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 699fe2c4c4d4d..47b8aaefaf86a 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -171,6 +171,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` Object { "baseUrl": "/", "category": Object { + "euiIconType": "managementApp", "id": "management", "label": "Management", "order": 5000, @@ -1606,6 +1607,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` + + + + +
+ +
+ +
+
- +
- +
- +
; +export const DEFAULT_APP_CATEGORIES: Record; // @public (undocumented) export interface DocLinksStart { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 24d1fc9d369f2..e136c699f7246 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -323,6 +323,7 @@ export { MetricsServiceStart, } from './metrics'; +export { AppCategory } from '../types'; export { DEFAULT_APP_CATEGORIES } from '../utils'; export { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 1dcf8a22e9cfd..11a14457784fd 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -164,6 +164,15 @@ import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; import { Url } from 'url'; +// @public +export interface AppCategory { + ariaLabel?: string; + euiIconType?: string; + id: string; + label: string; + order?: number; +} + // Warning: (ae-forgotten-export) The symbol "ConsoleAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FileAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "LegacyAppenderConfig" needs to be exported by the entry point index.d.ts @@ -484,37 +493,7 @@ export interface CustomHttpResponseOptions; +export const DEFAULT_APP_CATEGORIES: Record; // @public (undocumented) export interface DeleteDocumentResponse { diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index 1fb7c284c0dfd..809aaddb74172 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -18,9 +18,10 @@ */ import { i18n } from '@kbn/i18n'; +import { AppCategory } from '../types'; /** @internal */ -export const DEFAULT_APP_CATEGORIES = Object.freeze({ +export const DEFAULT_APP_CATEGORIES: Record = Object.freeze({ kibana: { id: 'kibana', label: i18n.translate('core.ui.kibanaNavList.label', { @@ -59,5 +60,6 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ defaultMessage: 'Management', }), order: 5000, + euiIconType: 'managementApp', }, }); diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json b/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json index 30e78635ec2e9..017d208133cdc 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/ingestion_pipeline_painless.json @@ -1 +1 @@ -{"description":"Kibana code coverage team assignments","processors":[{"script":{"lang":"painless","source":"\n String path = ctx.coveredFilePath; \n if (path.indexOf('src/legacy/core_plugins/kibana/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/kibana/common/utils') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/migrations') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dev_tools/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home/np_ready/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/local_application_service/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib/management/saved_objects') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/import/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/export/') == 0) ctx.team = 'kibana-platform';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/core_plugins/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/legacy/core_plugins/console_legacy') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/elasticsearch') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/embeddable_api/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/input_control_vis') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/region_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/status_page/public') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/testbed') == 0) ctx.team = 'kibana-platform';\n // else if (path.indexOf('src/legacy/core_plugins/tests_bundle/') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('src/legacy/core_plugins/tile_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/ui_metric/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_tagcloud') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vega') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vislib/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/server/') == 0) {\n\n if (path.indexOf('src/legacy/server/config/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/http/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/legacy/server/index_patterns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/server/keystore/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/logging/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/pid/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/sample_data/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/sass/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/saved_objects/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/status/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/url_shortening/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/utils/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/warnings/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/ui') == 0) {\n\n if (path.indexOf('src/legacy/ui/public/field_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/timefilter') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/management') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/state_management') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/ui/public/new_platform') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/plugin_discovery') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/chrome') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/notify') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/documentation_links') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/autoload') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/capabilities') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/legacy/ui/public/apm') == 0) ctx.team = 'apm-ui';\n\n } else if (path.indexOf('src/plugins/') == 0) {\n\n if (path.indexOf('src/plugins/advanced_settings/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/plugins/bfetch/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/public/static/color_maps') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/console/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/data/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/dev_tools/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/embeddable/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/expressions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/home/public') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/server/tutorials') == 0) ctx.team = 'observability';\n else if (path.indexOf('src/plugins/home/server/services/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/index_pattern_management/public/service') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/index_pattern_management/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/input_control_vis/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/inspector/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_legacy/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/kibana_react/public/code_editor') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('src/plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_utils/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/legacy_export/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/maps_legacy/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/region_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/tile_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/navigation/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/saved_objects_management/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/saved_objects/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/share/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/status_page/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/telemetry') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/testbed/server/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/vis_default_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/vis_type') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/visualize/') == 0) ctx.team = 'kibana-app';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/legacy/') == 0) {\n\n if (path.indexOf('x-pack/legacy/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/alerting/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/legacy/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/legacy/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/legacy/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/dashboard_mode/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/legacy/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/legacy/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/legacy/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/legacy/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/legacy/plugins/monitoring/') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/legacy/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/legacy/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/legacy/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/task_manager') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/uptime') == 0) ctx.team = 'uptime';\n else if (path.indexOf('x-pack/legacy/plugins/xpack_main/server/') == 0) ctx.team = 'kibana-platform';\n\n else if (path.indexOf('x-pack/legacy/server/lib/create_router/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/server/lib/check_license/') == 0) ctx.team = 'es-ui'; \n else if (path.indexOf('x-pack/legacy/server/lib/') == 0) ctx.team = 'kibana-platform'; \n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/plugins/') == 0) {\n\n if (path.indexOf('x-pack/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/advanced_ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/alerts') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/alerting_builtins') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/plugins/case') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/cloud/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/code/') == 0) ctx.team = 'code';\n else if (path.indexOf('x-pack/plugins/console_extensions/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/dashboard_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/dashboard_mode') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/discover_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/embeddable_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/data_enhanced/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/drilldowns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/endpoint/') == 0) ctx.team = 'endpoint-app-team';\n else if (path.indexOf('x-pack/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/event_log/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/features/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/file_upload') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/global_search') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('x-pack/plugins/graph/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/grokdebugger/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_lifecycle_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/plugins/ingest_pipelines/') == 0) ctx.team = 'es-ui';\n \n else if (path.indexOf('x-pack/plugins/lens/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/licensing/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/lists/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/logstash') == 0) ctx.team = 'logstash';\n else if (path.indexOf('x-pack/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/maps_legacy_licensing') == 0) ctx.team = 'maps';\n else if (path.indexOf('x-pack/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/monitoring') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/plugins/observability/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/oss_telemetry/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/painless_lab/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/remote_clusters/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/searchprofiler/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/security_solution/') == 0) ctx.team = 'siem';\n \n else if (path.indexOf('x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/task_manager/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/telemetry_collection_xpack/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/transform/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/translations/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('x-pack/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/ui_actions_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/uptime') == 0) ctx.team = 'uptime';\n \n else if (path.indexOf('x-pack/plugins/watcher/') == 0) ctx.team = 'es-ui';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('packages') == 0) {\n\n if (path.indexOf('packages/kbn-analytics/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('packages/kbn-babel') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-config-schema/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/elastic-datemath') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-dev-utils') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-es/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-eslint') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-expect') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('packages/kbn-interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-optimizer/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-pm/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test-subj-selector/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-ui-framework/') == 0) ctx.team = 'kibana-design';\n else if (path.indexOf('packages/kbn-ui-shared-deps/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else {\n\n if (path.indexOf('config/kibana.yml') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/apm.js') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/core/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/core/public/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/core/server/csp/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/dev/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/dev/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/dev/run_check_published_api_changes.ts') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/kbn-es-archiver/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/optimize/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/setup_node_env/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/test_utils/') == 0) ctx.team = 'kibana-operations'; \n else ctx.team = 'unknown';\n }"}}]} +{"description":"Kibana code coverage team assignments","processors":[{"script":{"lang":"painless","source":"\n String path = ctx.coveredFilePath; \n if (path.indexOf('src/legacy/core_plugins/kibana/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/kibana/common/utils') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/migrations') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/dev_tools/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/home/np_ready/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/local_application_service/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/kibana/public/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/lib/management/saved_objects') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/import/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/kibana/server/routes/api/export/') == 0) ctx.team = 'kibana-platform';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/core_plugins/') == 0) {\n\n if (path.indexOf('src/legacy/core_plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/legacy/core_plugins/console_legacy') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/elasticsearch') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/core_plugins/embeddable_api/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/input_control_vis') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/legacy/core_plugins/region_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/status_page/public') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/core_plugins/testbed') == 0) ctx.team = 'kibana-platform';\n // else if (path.indexOf('src/legacy/core_plugins/tests_bundle/') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('src/legacy/core_plugins/tile_map') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/legacy/core_plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/ui_metric/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_tagcloud') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vega') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/vis_type_vislib/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/core_plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/server/') == 0) {\n\n if (path.indexOf('src/legacy/server/config/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/http/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/legacy/server/index_patterns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/server/keystore/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/logging/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/pid/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/sample_data/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/sass/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/saved_objects/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/status/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/server/url_shortening/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/server/utils/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/legacy/server/warnings/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('src/legacy/ui') == 0) {\n\n if (path.indexOf('src/legacy/ui/public/field_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/timefilter') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/management') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/legacy/ui/public/state_management') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/legacy/ui/public/new_platform') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/plugin_discovery') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/chrome') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/notify') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/documentation_links') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/autoload') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/legacy/ui/public/capabilities') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/legacy/ui/public/apm') == 0) ctx.team = 'apm-ui';\n\n } else if (path.indexOf('src/plugins/') == 0) {\n\n if (path.indexOf('src/plugins/advanced_settings/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/apm_oss/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/plugins/bfetch/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/charts/public/static/color_maps') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/console/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/dashboard/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/data/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/dev_tools/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/discover/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/embeddable/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('src/plugins/expressions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/home/public') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/server/tutorials') == 0) ctx.team = 'observability';\n else if (path.indexOf('src/plugins/home/server/services/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/home/') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/index_pattern_management/public/service') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/index_pattern_management/public') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/input_control_vis/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/inspector/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_legacy/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/kibana_react/public/code_editor') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('src/plugins/kibana_react/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_utils/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/management/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/kibana_usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/legacy_export/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/maps_legacy/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/region_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/tile_map/') == 0) ctx.team = 'maps';\n else if (path.indexOf('src/plugins/timelion') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/navigation/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/newsfeed') == 0) ctx.team = 'kibana-core-ui';\n else if (path.indexOf('src/plugins/saved_objects_management/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/saved_objects/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/share/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/status_page/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/telemetry') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/testbed/server/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/plugins/ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/usage_collection/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('src/plugins/vis_default_editor') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/vis_type') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('src/plugins/visualizations/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('src/plugins/visualize/') == 0) ctx.team = 'kibana-app';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/legacy/') == 0) {\n\n if (path.indexOf('x-pack/legacy/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/alerting/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/legacy/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/legacy/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/legacy/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/dashboard_mode/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/legacy/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/legacy/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/legacy/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/legacy/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/legacy/plugins/monitoring/') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/legacy/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/legacy/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/legacy/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/legacy/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/task_manager') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/legacy/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/plugins/uptime') == 0) ctx.team = 'uptime';\n else if (path.indexOf('x-pack/legacy/plugins/xpack_main/server/') == 0) ctx.team = 'kibana-platform';\n\n else if (path.indexOf('x-pack/legacy/server/lib/create_router/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/legacy/server/lib/check_license/') == 0) ctx.team = 'es-ui'; \n else if (path.indexOf('x-pack/legacy/server/lib/') == 0) ctx.team = 'kibana-platform'; \n else ctx.team = 'unknown';\n\n } else if (path.indexOf('x-pack/plugins/') == 0) {\n\n if (path.indexOf('x-pack/plugins/actions/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/advanced_ui_actions/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/alerts') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/alerting_builtins') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/apm/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/beats_management/') == 0) ctx.team = 'beats';\n else if (path.indexOf('x-pack/plugins/canvas/') == 0) ctx.team = 'kibana-canvas';\n else if (path.indexOf('x-pack/plugins/case') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/cloud/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/code/') == 0) ctx.team = 'code';\n else if (path.indexOf('x-pack/plugins/console_extensions/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/cross_cluster_replication/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/dashboard_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/dashboard_mode') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/discover_enhanced') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/embeddable_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/data_enhanced/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/drilldowns/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/encrypted_saved_objects/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/endpoint/') == 0) ctx.team = 'endpoint-app-team';\n else if (path.indexOf('x-pack/plugins/es_ui_shared/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/event_log/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/features/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/file_upload') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/global_search') == 0) ctx.team = 'kibana-platform';\n \n else if (path.indexOf('x-pack/plugins/graph/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/grokdebugger/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_lifecycle_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/index_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/infra/') == 0) ctx.team = 'logs-metrics-ui';\n else if (path.indexOf('x-pack/plugins/ingest_manager/') == 0) ctx.team = 'ingest-management';\n else if (path.indexOf('x-pack/plugins/ingest_pipelines/') == 0) ctx.team = 'es-ui';\n \n else if (path.indexOf('x-pack/plugins/lens/') == 0) ctx.team = 'kibana-app';\n else if (path.indexOf('x-pack/plugins/license_management/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/licensing/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('x-pack/plugins/lists/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/logstash') == 0) ctx.team = 'logstash';\n else if (path.indexOf('x-pack/plugins/maps/') == 0) ctx.team = 'kibana-gis';\n else if (path.indexOf('x-pack/plugins/maps_legacy_licensing') == 0) ctx.team = 'maps';\n else if (path.indexOf('x-pack/plugins/ml/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/monitoring') == 0) ctx.team = 'stack-monitoring-ui';\n else if (path.indexOf('x-pack/plugins/observability/') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('x-pack/plugins/oss_telemetry/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/painless_lab/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/remote_clusters/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/reporting') == 0) ctx.team = 'kibana-reporting';\n else if (path.indexOf('x-pack/plugins/rollup/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/searchprofiler/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/security/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/security_solution/') == 0) ctx.team = 'siem';\n \n else if (path.indexOf('x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules') == 0) ctx.team = 'security-intelligence-analytics';\n else if (path.indexOf('x-pack/plugins/siem/') == 0) ctx.team = 'siem';\n else if (path.indexOf('x-pack/plugins/snapshot_restore/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/spaces/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('x-pack/plugins/task_manager/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/telemetry_collection_xpack/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('x-pack/plugins/transform/') == 0) ctx.team = 'ml-ui';\n else if (path.indexOf('x-pack/plugins/translations/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('x-pack/plugins/triggers_actions_ui/') == 0) ctx.team = 'kibana-alerting-services';\n else if (path.indexOf('x-pack/plugins/upgrade_assistant/') == 0) ctx.team = 'es-ui';\n else if (path.indexOf('x-pack/plugins/ui_actions_enhanced') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('x-pack/plugins/uptime') == 0) ctx.team = 'uptime';\n \n else if (path.indexOf('x-pack/plugins/watcher/') == 0) ctx.team = 'es-ui';\n else ctx.team = 'unknown';\n\n } else if (path.indexOf('packages') == 0) {\n\n if (path.indexOf('packages/kbn-analytics/') == 0) ctx.team = 'pulse';\n else if (path.indexOf('packages/kbn-babel') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-config-schema/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/elastic-datemath') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-dev-utils') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-es/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-eslint') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-expect') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('packages/kbn-interpreter/') == 0) ctx.team = 'kibana-app-arch';\n else if (path.indexOf('packages/kbn-optimizer/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-pm/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-test-subj-selector/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('packages/kbn-ui-framework/') == 0) ctx.team = 'kibana-design';\n else if (path.indexOf('packages/kbn-ui-shared-deps/') == 0) ctx.team = 'kibana-operations';\n else ctx.team = 'unknown';\n\n } else {\n\n if (path.indexOf('config/kibana.yml') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/apm.js') == 0) ctx.team = 'apm-ui';\n else if (path.indexOf('src/core/') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('src/core/public/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/core/server/csp/') == 0) ctx.team = 'kibana-security';\n else if (path.indexOf('src/dev/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/dev/i18n/') == 0) ctx.team = 'kibana-localization';\n else if (path.indexOf('src/dev/run_check_published_api_changes.ts') == 0) ctx.team = 'kibana-platform';\n else if (path.indexOf('packages/kbn-es-archiver/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/optimize/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/setup_node_env/') == 0) ctx.team = 'kibana-operations';\n else if (path.indexOf('src/test_utils/') == 0) ctx.team = 'kibana-operations'; \n else ctx.team = 'unknown';\n }"}}]} diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 486c8563c5456..5d31db63773fa 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -25,7 +25,6 @@ export default { '/src/plugins', '/src/legacy/ui', '/src/core', - '/src/legacy/core_plugins', '/src/legacy/server', '/src/cli', '/src/cli_keystore', @@ -51,14 +50,11 @@ export default { 'packages/kbn-ui-framework/src/services/**/*.js', '!packages/kbn-ui-framework/src/services/index.js', '!packages/kbn-ui-framework/src/services/**/*/index.js', - 'src/legacy/core_plugins/**/*.{js,mjs,jsx,ts,tsx}', - '!src/legacy/core_plugins/**/{__test__,__snapshots__}/**/*', ], moduleNameMapper: { '@elastic/eui$': '/node_modules/@elastic/eui/test-env', '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '^src/plugins/(.*)': '/src/plugins/$1', - '^plugins/([^/.]*)(.*)': '/src/legacy/core_plugins/$1/public$2', '^uiExports/(.*)': '/src/dev/jest/mocks/file_mock.js', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index e22dc03cf57aa..ba58dcdfa4d58 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -132,11 +132,6 @@ export const REMOVE_EXTENSION = ['packages/kbn-plugin-generator/template/**/*.ej * @type {Array} */ export const TEMPORARILY_IGNORED_PATHS = [ - 'src/legacy/core_plugins/console/public/src/directives/helpExample.txt', - 'src/legacy/core_plugins/console/public/src/sense_editor/theme-sense-dark.js', - 'src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png', - 'src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png', - 'src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png', 'src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json', 'src/core/server/core_app/assets/favicons/android-chrome-192x192.png', 'src/core/server/core_app/assets/favicons/android-chrome-256x256.png', diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts deleted file mode 100644 index 83e7bb19e57ba..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - Client as ESClient, - GenericParams, - // root params - BulkIndexDocumentsParams, - ClearScrollParams, - CountParams, - CreateDocumentParams, - DeleteDocumentParams, - DeleteDocumentByQueryParams, - DeleteScriptParams, - DeleteTemplateParams, - ExistsParams, - ExplainParams, - FieldStatsParams, - GetParams, - GetResponse, - GetScriptParams, - GetSourceParams, - GetTemplateParams, - IndexDocumentParams, - InfoParams, - MGetParams, - MSearchParams, - MSearchTemplateParams, - MTermVectorsParams, - PingParams, - PutScriptParams, - PutTemplateParams, - ReindexParams, - ReindexRethrottleParams, - RenderSearchTemplateParams, - ScrollParams, - SearchParams, - SearchShardsParams, - SearchTemplateParams, - SuggestParams, - TermvectorsParams, - UpdateDocumentParams, - UpdateDocumentByQueryParams, - MGetResponse, - MSearchResponse, - SearchResponse, - // cat - CatAliasesParams, - CatAllocationParams, - CatFielddataParams, - CatHealthParams, - CatHelpParams, - CatIndicesParams, - CatCommonParams, - CatRecoveryParams, - CatSegmentsParams, - CatShardsParams, - CatSnapshotsParams, - CatTasksParams, - CatThreadPoolParams, - // cluster - ClusterAllocationExplainParams, - ClusterGetSettingsParams, - ClusterHealthParams, - ClusterPendingTasksParams, - ClusterPutSettingsParams, - ClusterRerouteParams, - ClusterStateParams, - ClusterStatsParams, - // indices - IndicesAnalyzeParams, - IndicesClearCacheParams, - IndicesCloseParams, - IndicesCreateParams, - IndicesDeleteParams, - IndicesDeleteAliasParams, - IndicesDeleteTemplateParams, - IndicesExistsParams, - IndicesExistsAliasParams, - IndicesExistsTemplateParams, - IndicesExistsTypeParams, - IndicesFlushParams, - IndicesFlushSyncedParams, - IndicesForcemergeParams, - IndicesGetParams, - IndicesGetAliasParams, - IndicesGetFieldMappingParams, - IndicesGetMappingParams, - IndicesGetSettingsParams, - IndicesGetTemplateParams, - IndicesGetUpgradeParams, - IndicesOpenParams, - IndicesPutAliasParams, - IndicesPutMappingParams, - IndicesPutSettingsParams, - IndicesPutTemplateParams, - IndicesRecoveryParams, - IndicesRefreshParams, - IndicesRolloverParams, - IndicesSegmentsParams, - IndicesShardStoresParams, - IndicesShrinkParams, - IndicesStatsParams, - IndicesUpdateAliasesParams, - IndicesUpgradeParams, - IndicesValidateQueryParams, - // ingest - IngestDeletePipelineParams, - IngestGetPipelineParams, - IngestPutPipelineParams, - IngestSimulateParams, - // nodes - NodesHotThreadsParams, - NodesInfoParams, - NodesStatsParams, - // snapshot - SnapshotCreateParams, - SnapshotCreateRepositoryParams, - SnapshotDeleteParams, - SnapshotDeleteRepositoryParams, - SnapshotGetParams, - SnapshotGetRepositoryParams, - SnapshotRestoreParams, - SnapshotStatusParams, - SnapshotVerifyRepositoryParams, - // tasks - TasksCancelParams, - TasksGetParams, - TasksListParams, -} from 'elasticsearch'; - -export class Cluster { - public callWithRequest: CallClusterWithRequest; - public callWithInternalUser: CallCluster; - constructor(config: ClusterConfig); -} - -export interface ClusterConfig { - [option: string]: any; -} - -export interface Request { - headers: RequestHeaders; -} - -interface RequestHeaders { - [name: string]: string; -} - -interface AssistantAPIClientParams extends GenericParams { - path: '/_migration/assistance'; - method: 'GET'; -} - -type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; - -export interface AssistanceAPIResponse { - indices: { - [indexName: string]: { - action_required: MIGRATION_ASSISTANCE_INDEX_ACTION; - }; - }; -} - -interface DeprecationAPIClientParams extends GenericParams { - path: '/_migration/deprecations'; - method: 'GET'; -} - -export interface DeprecationInfo { - level: MIGRATION_DEPRECATION_LEVEL; - message: string; - url: string; - details?: string; -} - -export interface IndexSettingsDeprecationInfo { - [indexName: string]: DeprecationInfo[]; -} - -export interface DeprecationAPIResponse { - cluster_settings: DeprecationInfo[]; - ml_settings: DeprecationInfo[]; - node_settings: DeprecationInfo[]; - index_settings: IndexSettingsDeprecationInfo; -} - -export interface CallClusterOptions { - wrap401Errors?: boolean; - signal?: AbortSignal; -} - -export interface CallClusterWithRequest { - /* eslint-disable */ - (request: Request, endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'clearScroll', params: ClearScrollParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'count', params: CountParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'create', params: CreateDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'delete', params: DeleteDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteScript', params: DeleteScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'exists', params: ExistsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'explain', params: ExplainParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'fieldStats', params: FieldStatsParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'get', params: GetParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'getScript', params: GetScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'getSource', params: GetSourceParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'getTemplate', params: GetTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'index', params: IndexDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'info', params: InfoParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'mget', params: MGetParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'msearch', params: MSearchParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'mtermvectors', params: MTermVectorsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ping', params: PingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'putScript', params: PutScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'putTemplate', params: PutTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'reindex', params: ReindexParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'scroll', params: ScrollParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'search', params: SearchParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'searchShards', params: SearchShardsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'searchTemplate', params: SearchTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'suggest', params: SuggestParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'termvectors', params: TermvectorsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'update', params: UpdateDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - - // cat namespace - (request: Request, endpoint: 'cat.aliases', params: CatAliasesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.allocation', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.count', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.fielddata', params: CatFielddataParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.health', params: CatHealthParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.help', params: CatHelpParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.indices', params: CatIndicesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.master', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.nodes', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.plugins', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.recovery', params: CatRecoveryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.repositories', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.segments', params: CatSegmentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.shards', params: CatShardsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.tasks', params: CatTasksParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: CallClusterOptions): ReturnType; - - // cluster namespace - (request: Request, endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.health', params: ClusterHealthParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.state', params: ClusterStateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.stats', params: ClusterStatsParams, options?: CallClusterOptions): ReturnType; - - // indices namespace - (request: Request, endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.close', params: IndicesCloseParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.create', params: IndicesCreateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.delete', params: IndicesDeleteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.exists', params: IndicesExistsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.flush', params: IndicesFlushParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.get', params: IndicesGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.open', params: IndicesOpenParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.stats', params: IndicesStatsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - - // ingest namepsace - (request: Request, endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: CallClusterOptions): ReturnType; - - // nodes namespace - (request: Request, endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'nodes.info', params: NodesInfoParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'nodes.stats', params: NodesStatsParams, options?: CallClusterOptions): ReturnType; - - // snapshot namespace - (request: Request, endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.get', params: SnapshotGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: CallClusterOptions): ReturnType; - - // tasks namespace - (request: Request, endpoint: 'tasks.cancel', params: TasksCancelParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'tasks.get', params: TasksGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'tasks.list', params: TasksListParams, options?: CallClusterOptions): ReturnType; - - // other APIs accessed via transport.request - ( - request: Request, - endpoint: 'transport.request', - clientParams: AssistantAPIClientParams, - options?: {} - ): Promise; - ( - request: Request, - endpoint: 'transport.request', - clientParams: DeprecationAPIClientParams, - options?: {} - ): Promise; - - // Catch-all definition - ( - request: Request, - endpoint: string, - clientParams?: any, - options?: CallClusterOptions - ): Promise; - /* eslint-enable */ -} - -export interface CallCluster { - /* eslint-disable */ - (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'clearScroll', params: ClearScrollParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'count', params: CountParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'create', params: CreateDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'delete', params: DeleteDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteScript', params: DeleteScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'exists', params: ExistsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'explain', params: ExplainParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'fieldStats', params: FieldStatsParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'get', params: GetParams, options?: CallClusterOptions): Promise>; - (endpoint: 'getScript', params: GetScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'getSource', params: GetSourceParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'getTemplate', params: GetTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'index', params: IndexDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'info', params: InfoParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'mget', params: MGetParams, options?: CallClusterOptions): Promise>; - (endpoint: 'msearch', params: MSearchParams, options?: CallClusterOptions): Promise>; - (endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: CallClusterOptions): Promise>; - (endpoint: 'mtermvectors', params: MTermVectorsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ping', params: PingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'putScript', params: PutScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'putTemplate', params: PutTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'reindex', params: ReindexParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'scroll', params: ScrollParams, options?: CallClusterOptions): Promise>; - (endpoint: 'search', params: SearchParams, options?: CallClusterOptions): Promise>; - (endpoint: 'searchShards', params: SearchShardsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'searchTemplate', params: SearchTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'suggest', params: SuggestParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'termvectors', params: TermvectorsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'update', params: UpdateDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - - // cat namespace - (endpoint: 'cat.aliases', params: CatAliasesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.allocation', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.count', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.fielddata', params: CatFielddataParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.health', params: CatHealthParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.help', params: CatHelpParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.indices', params: CatIndicesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.master', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.nodes', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.plugins', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.recovery', params: CatRecoveryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.repositories', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.segments', params: CatSegmentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.shards', params: CatShardsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.tasks', params: CatTasksParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: CallClusterOptions): ReturnType; - - // cluster namespace - (endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.health', params: ClusterHealthParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.state', params: ClusterStateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.stats', params: ClusterStatsParams, options?: CallClusterOptions): ReturnType; - - // indices namespace - (endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.close', params: IndicesCloseParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.create', params: IndicesCreateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.delete', params: IndicesDeleteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.exists', params: IndicesExistsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.flush', params: IndicesFlushParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.get', params: IndicesGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.open', params: IndicesOpenParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.stats', params: IndicesStatsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - - // ingest namespace - (endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: CallClusterOptions): ReturnType; - - // nodes namespace - (endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'nodes.info', params: NodesInfoParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'nodes.stats', params: NodesStatsParams, options?: CallClusterOptions): ReturnType; - - // snapshot namespace - (endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.get', params: SnapshotGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: CallClusterOptions): ReturnType; - - // tasks namespace - (endpoint: 'tasks.cancel', params: TasksCancelParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'tasks.get', params: TasksGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'tasks.list', params: TasksListParams, options?: CallClusterOptions): ReturnType; - - // other APIs accessed via transport.request - (endpoint: 'transport.request', clientParams: AssistantAPIClientParams, options?: {}): Promise< - AssistanceAPIResponse - >; - (endpoint: 'transport.request', clientParams: DeprecationAPIClientParams, options?: {}): Promise< - DeprecationAPIResponse - >; - - // Catch-all definition - (endpoint: string, clientParams?: any, options?: CallClusterOptions): Promise; - /* eslint-enable */ -} - -export interface ElasticsearchPlugin { - status: { on: (status: string, cb: () => void) => void }; - getCluster(name: string): Cluster; -} diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js deleted file mode 100644 index f90f490d68035..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { Cluster } from './server/lib/cluster'; -import { createProxy } from './server/lib/create_proxy'; - -export default function (kibana) { - return new kibana.Plugin({ - require: [], - - async init(server) { - // All methods that ES plugin exposes are synchronous so we should get the first - // value from all observables here to be able to synchronously return and create - // cluster clients afterwards. - const { client } = server.newPlatform.setup.core.elasticsearch.legacy; - const adminCluster = new Cluster(client); - const dataCluster = new Cluster(client); - - const clusters = new Map(); - server.expose('getCluster', (name) => { - if (name === 'admin') { - return adminCluster; - } - - if (name === 'data') { - return dataCluster; - } - - return clusters.get(name); - }); - - server.events.on('stop', () => { - for (const cluster of clusters.values()) { - cluster.close(); - } - - clusters.clear(); - }); - - createProxy(server); - }, - }); -} diff --git a/src/legacy/core_plugins/elasticsearch/package.json b/src/legacy/core_plugins/elasticsearch/package.json deleted file mode 100644 index b5403e1f13de7..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "elasticsearch", - "version": "kibana", - "types": "index.d.ts" -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js b/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js deleted file mode 100644 index d79dd4ae4e449..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AbortSignal } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; -import { abortableRequestHandler } from './abortable_request_handler'; - -describe('abortableRequestHandler', () => { - jest.useFakeTimers(); - - it('should call abort if disconnected', () => { - const eventHandlers = new Map(); - const mockRequest = { - events: { - once: jest.fn((key, fn) => eventHandlers.set(key, fn)), - }, - }; - - const handler = jest.fn(); - const onAborted = jest.fn(); - const abortableHandler = abortableRequestHandler(handler); - abortableHandler(mockRequest); - - const [signal, request] = handler.mock.calls[0]; - - expect(signal instanceof AbortSignal).toBe(true); - expect(request).toBe(mockRequest); - - signal.addEventListener('abort', onAborted); - - // Shouldn't be aborted or call onAborted prior to disconnecting - expect(signal.aborted).toBe(false); - expect(onAborted).not.toBeCalled(); - - expect(eventHandlers.has('disconnect')).toBe(true); - eventHandlers.get('disconnect')(); - jest.runAllTimers(); - - // Should be aborted and call onAborted after disconnecting - expect(signal.aborted).toBe(true); - expect(onAborted).toBeCalled(); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts b/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts deleted file mode 100644 index 0e7692f6be755..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Request } from 'hapi'; -import { errors } from 'elasticsearch'; -import { LegacyCallAPIOptions, LegacyClusterClient, FakeRequest } from 'kibana/server'; - -export class Cluster { - public readonly errors = errors; - - constructor(private readonly clusterClient: LegacyClusterClient) {} - - public callWithRequest = async ( - req: Request | FakeRequest, - endpoint: string, - clientParams?: Record, - options?: LegacyCallAPIOptions - ) => { - return await this.clusterClient - .asScoped(req) - .callAsCurrentUser(endpoint, clientParams, options); - }; - - public callWithInternalUser = async ( - endpoint: string, - clientParams?: Record, - options?: LegacyCallAPIOptions - ) => { - return await this.clusterClient.callAsInternalUser(endpoint, clientParams, options); - }; - - public close() { - this.clusterClient.close(); - } -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js b/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js deleted file mode 100644 index 7302241c46939..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Joi from 'joi'; -import { abortableRequestHandler } from './abortable_request_handler'; - -export function createProxy(server) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - - server.route({ - method: 'POST', - path: '/elasticsearch/_msearch', - config: { - payload: { - parse: 'gunzip', - }, - }, - handler: abortableRequestHandler((signal, req, h) => { - const { query, payload } = req; - return callWithRequest( - req, - 'transport.request', - { - path: '/_msearch', - method: 'POST', - query, - body: payload.toString('utf8'), - }, - { signal } - ).finally((r) => h.response(r)); - }), - }); - - server.route({ - method: 'POST', - path: '/elasticsearch/{index}/_search', - config: { - validate: { - params: Joi.object().keys({ - index: Joi.string().required(), - }), - }, - }, - handler: abortableRequestHandler(async (signal, req) => { - const { query, payload: body } = req; - try { - return await callWithRequest( - req, - 'transport.request', - { - path: `/${encodeURIComponent(req.params.index)}/_search`, - method: 'POST', - query, - body, - }, - { signal } - ); - } catch (error) { - return JSON.parse(error.response); - } - }), - }); -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 8827dc53c5275..3cfda0e0696bb 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -33,7 +33,6 @@ import { import { LegacyConfig, ILegacyInternals } from '../../core/server/legacy'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiPlugins } from '../../core/server/plugins'; -import { ElasticsearchPlugin } from '../core_plugins/elasticsearch'; // lot of legacy code was assuming this type only had these two methods export type KibanaConfig = Pick; @@ -41,10 +40,7 @@ export type KibanaConfig = Pick; // Extend the defaults with the plugins and server methods we need. declare module 'hapi' { interface PluginProperties { - elasticsearch: ElasticsearchPlugin; - kibana: any; spaces: any; - // add new plugin types here } interface Server { diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts index 28ba0ab629e8f..317d0f0140293 100644 --- a/src/plugins/data/common/es_query/filters/get_display_value.ts +++ b/src/plugins/data/common/es_query/filters/get_display_value.ts @@ -43,7 +43,7 @@ export function getDisplayValueFromFilter(filter: Filter, indexPatterns: IIndexP if (typeof filter.meta.value === 'function') { const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); const valueFormatter: any = getValueFormatter(indexPattern, filter.meta.key); - return filter.meta.value(valueFormatter); + return (filter.meta.value as any)(valueFormatter); } else { return filter.meta.value || ''; } diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts index e3099ae6a4026..1e892d452f401 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/src/plugins/data/common/es_query/filters/meta_filter.ts @@ -22,9 +22,10 @@ export enum FilterStateStore { GLOBAL_STATE = 'globalState', } -export interface FilterState { +// eslint-disable-next-line +export type FilterState = { store: FilterStateStore; -} +}; type FilterFormatterFunction = (value: any) => string; export interface FilterValueFormatter { @@ -32,7 +33,8 @@ export interface FilterValueFormatter { getConverterFor: (type: string) => FilterFormatterFunction; } -export interface FilterMeta { +// eslint-disable-next-line +export type FilterMeta = { alias: string | null; disabled: boolean; negate: boolean; @@ -43,14 +45,15 @@ export interface FilterMeta { type?: string; key?: string; params?: any; - value?: string | ((formatter?: FilterValueFormatter) => string); -} + value?: string; +}; -export interface Filter { +// eslint-disable-next-line +export type Filter = { $state?: FilterState; meta: FilterMeta; query?: any; -} +}; export interface LatLon { lat: number; diff --git a/src/plugins/data/common/query/timefilter/types.ts b/src/plugins/data/common/query/timefilter/types.ts index 60008ce6054e1..82b1ae69cc73b 100644 --- a/src/plugins/data/common/query/timefilter/types.ts +++ b/src/plugins/data/common/query/timefilter/types.ts @@ -24,11 +24,12 @@ export interface RefreshInterval { value: number; } -export interface TimeRange { +// eslint-disable-next-line +export type TimeRange = { from: string; to: string; mode?: 'absolute' | 'relative'; -} +}; export interface TimeRangeBounds { min: Moment | undefined; diff --git a/src/plugins/data/common/query/types.ts b/src/plugins/data/common/query/types.ts index 6b34a1baf293b..c1a98eac5350e 100644 --- a/src/plugins/data/common/query/types.ts +++ b/src/plugins/data/common/query/types.ts @@ -19,7 +19,8 @@ export * from './timefilter/types'; -export interface Query { +// eslint-disable-next-line +export type Query = { query: string | { [key: string]: any }; language: string; -} +}; diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts index fd788d3339295..d3a95b32cd425 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts @@ -139,6 +139,19 @@ describe('calculateHistogramInterval', () => { }) ).toEqual(0.02); }); + + test('should correctly fallback to the default value for empty string', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: '', + values: { + min: 0.1, + max: 0.9, + }, + }) + ).toBe(0.01); + }); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts index f4e42fa8881ef..378340e876296 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts @@ -27,7 +27,7 @@ interface IntervalValuesRange { export interface CalculateHistogramIntervalParams { interval: string; maxBucketsUiSettings: number; - maxBucketsUserInput?: number; + maxBucketsUserInput?: number | ''; intervalBase?: number; values?: IntervalValuesRange; } @@ -77,12 +77,7 @@ const calculateForGivenInterval = ( - The lower power of 10, times 2 - The lower power of 10, times 5 **/ -const calculateAutoInterval = ( - diff: number, - maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'], - maxBucketsUserInput: CalculateHistogramIntervalParams['maxBucketsUserInput'] -) => { - const maxBars = Math.min(maxBucketsUiSettings, maxBucketsUserInput ?? maxBucketsUiSettings); +const calculateAutoInterval = (diff: number, maxBars: number) => { const exactInterval = diff / maxBars; const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); @@ -122,7 +117,11 @@ export const calculateHistogramInterval = ({ if (diff) { calculatedInterval = isAuto - ? calculateAutoInterval(diff, maxBucketsUiSettings, maxBucketsUserInput) + ? calculateAutoInterval( + diff, + // Mind maxBucketsUserInput can be an empty string, hence we need to ensure it here + Math.min(maxBucketsUiSettings, maxBucketsUserInput || maxBucketsUiSettings) + ) : calculateForGivenInterval(diff, calculatedInterval, maxBucketsUiSettings); } } diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 061974d860246..2ee0db384cf06 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -20,6 +20,6 @@ export * from './aggs'; export * from './es_search'; export * from './expressions'; +export * from './search_source'; export * from './tabify'; export * from './types'; -export * from './es_search'; diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/common/search/search_source/create_search_source.test.ts similarity index 96% rename from src/plugins/data/public/search/search_source/create_search_source.test.ts rename to src/plugins/data/common/search/search_source/create_search_source.test.ts index 6b6cfb0c9b1ca..dde5983fe73fb 100644 --- a/src/plugins/data/public/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/common/search/search_source/create_search_source.test.ts @@ -19,9 +19,9 @@ import { createSearchSource as createSearchSourceFactory } from './create_search_source'; import { SearchSourceDependencies } from './search_source'; -import { IIndexPattern } from '../../../common/index_patterns'; +import { IIndexPattern } from '../../index_patterns'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; -import { Filter } from '../../../common/es_query/filters'; +import { Filter } from '../../es_query/filters'; import { BehaviorSubject } from 'rxjs'; describe('createSearchSource', () => { diff --git a/src/plugins/data/public/search/search_source/create_search_source.ts b/src/plugins/data/common/search/search_source/create_search_source.ts similarity index 100% rename from src/plugins/data/public/search/search_source/create_search_source.ts rename to src/plugins/data/common/search/search_source/create_search_source.ts diff --git a/src/plugins/data/public/search/search_source/extract_references.ts b/src/plugins/data/common/search/search_source/extract_references.ts similarity index 94% rename from src/plugins/data/public/search/search_source/extract_references.ts rename to src/plugins/data/common/search/search_source/extract_references.ts index f9987767a9688..72d93e41305d1 100644 --- a/src/plugins/data/public/search/search_source/extract_references.ts +++ b/src/plugins/data/common/search/search_source/extract_references.ts @@ -17,8 +17,8 @@ * under the License. */ -import { SavedObjectReference } from '../../../../../core/types'; -import { Filter } from '../../../common/es_query/filters'; +import { SavedObjectReference } from 'src/core/types'; +import { Filter } from '../../es_query/filters'; import { SearchSourceFields } from './types'; export const extractReferences = ( diff --git a/src/plugins/data/public/search/fetch/get_search_params.test.ts b/src/plugins/data/common/search/search_source/fetch/get_search_params.test.ts similarity index 93% rename from src/plugins/data/public/search/fetch/get_search_params.test.ts rename to src/plugins/data/common/search/search_source/fetch/get_search_params.test.ts index 5e83e1f57bb6d..8778eb4fd559d 100644 --- a/src/plugins/data/public/search/fetch/get_search_params.test.ts +++ b/src/plugins/data/common/search/search_source/fetch/get_search_params.test.ts @@ -17,8 +17,9 @@ * under the License. */ +import { UI_SETTINGS } from '../../../constants'; +import { GetConfigFn } from '../../../types'; import { getSearchParams } from './get_search_params'; -import { GetConfigFn, UI_SETTINGS } from '../../../common'; function getConfigStub(config: any = {}): GetConfigFn { return (key) => config[key]; diff --git a/src/plugins/data/public/search/fetch/get_search_params.ts b/src/plugins/data/common/search/search_source/fetch/get_search_params.ts similarity index 92% rename from src/plugins/data/public/search/fetch/get_search_params.ts rename to src/plugins/data/common/search/search_source/fetch/get_search_params.ts index ed87c4813951c..556fb4924da56 100644 --- a/src/plugins/data/public/search/fetch/get_search_params.ts +++ b/src/plugins/data/common/search/search_source/fetch/get_search_params.ts @@ -17,7 +17,9 @@ * under the License. */ -import { UI_SETTINGS, ISearchRequestParams, GetConfigFn } from '../../../common'; +import { UI_SETTINGS } from '../../../constants'; +import { GetConfigFn } from '../../../types'; +import { ISearchRequestParams } from '../../index'; import { SearchRequest } from './types'; const sessionId = Date.now(); diff --git a/src/plugins/data/common/search/search_source/fetch/index.ts b/src/plugins/data/common/search/search_source/fetch/index.ts new file mode 100644 index 0000000000000..1b9a9677e4a99 --- /dev/null +++ b/src/plugins/data/common/search/search_source/fetch/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getSearchParams, getSearchParamsFromRequest, getPreference } from './get_search_params'; +export { RequestFailure } from './request_error'; +export * from './types'; diff --git a/src/plugins/data/public/search/fetch/request_error.ts b/src/plugins/data/common/search/search_source/fetch/request_error.ts similarity index 95% rename from src/plugins/data/public/search/fetch/request_error.ts rename to src/plugins/data/common/search/search_source/fetch/request_error.ts index efaaafadf404e..ba5eb6f2897a9 100644 --- a/src/plugins/data/public/search/fetch/request_error.ts +++ b/src/plugins/data/common/search/search_source/fetch/request_error.ts @@ -18,7 +18,7 @@ */ import { SearchResponse } from 'elasticsearch'; -import { KbnError } from '../../../../kibana_utils/common'; +import { KbnError } from '../../../../../kibana_utils/common'; import { SearchError } from './types'; /** diff --git a/src/plugins/data/public/search/fetch/types.ts b/src/plugins/data/common/search/search_source/fetch/types.ts similarity index 97% rename from src/plugins/data/public/search/fetch/types.ts rename to src/plugins/data/common/search/search_source/fetch/types.ts index cdf10d8f1a1b0..30055f88012f2 100644 --- a/src/plugins/data/public/search/fetch/types.ts +++ b/src/plugins/data/common/search/search_source/fetch/types.ts @@ -18,8 +18,8 @@ */ import { SearchResponse } from 'elasticsearch'; -import { GetConfigFn } from '../../../common'; import { LegacyFetchHandlers } from '../legacy/types'; +import { GetConfigFn } from '../../../types'; /** * @internal diff --git a/src/plugins/data/public/search/search_source/filter_docvalue_fields.test.ts b/src/plugins/data/common/search/search_source/filter_docvalue_fields.test.ts similarity index 100% rename from src/plugins/data/public/search/search_source/filter_docvalue_fields.test.ts rename to src/plugins/data/common/search/search_source/filter_docvalue_fields.test.ts diff --git a/src/plugins/data/public/search/search_source/filter_docvalue_fields.ts b/src/plugins/data/common/search/search_source/filter_docvalue_fields.ts similarity index 100% rename from src/plugins/data/public/search/search_source/filter_docvalue_fields.ts rename to src/plugins/data/common/search/search_source/filter_docvalue_fields.ts diff --git a/src/plugins/data/public/search/search_source/index.ts b/src/plugins/data/common/search/search_source/index.ts similarity index 95% rename from src/plugins/data/public/search/search_source/index.ts rename to src/plugins/data/common/search/search_source/index.ts index 48c0338f7e981..70c9cfcee2348 100644 --- a/src/plugins/data/public/search/search_source/index.ts +++ b/src/plugins/data/common/search/search_source/index.ts @@ -23,3 +23,5 @@ export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types'; export { injectReferences } from './inject_references'; export { extractReferences } from './extract_references'; export { parseSearchSourceJSON } from './parse_json'; +export * from './fetch'; +export * from './legacy'; diff --git a/src/plugins/data/public/search/search_source/inject_references.ts b/src/plugins/data/common/search/search_source/inject_references.ts similarity index 96% rename from src/plugins/data/public/search/search_source/inject_references.ts rename to src/plugins/data/common/search/search_source/inject_references.ts index 07f37c3c11275..81fafc6dcae06 100644 --- a/src/plugins/data/public/search/search_source/inject_references.ts +++ b/src/plugins/data/common/search/search_source/inject_references.ts @@ -17,8 +17,8 @@ * under the License. */ +import { SavedObjectReference } from 'src/core/types'; import { SearchSourceFields } from './types'; -import { SavedObjectReference } from '../../../../../core/types'; export const injectReferences = ( searchSourceFields: SearchSourceFields & { indexRefName: string }, diff --git a/src/plugins/data/public/search/legacy/call_client.test.ts b/src/plugins/data/common/search/search_source/legacy/call_client.test.ts similarity index 100% rename from src/plugins/data/public/search/legacy/call_client.test.ts rename to src/plugins/data/common/search/search_source/legacy/call_client.test.ts diff --git a/src/plugins/data/public/search/legacy/call_client.ts b/src/plugins/data/common/search/search_source/legacy/call_client.ts similarity index 93% rename from src/plugins/data/public/search/legacy/call_client.ts rename to src/plugins/data/common/search/search_source/legacy/call_client.ts index b87affdd59c54..cb6295dd701ee 100644 --- a/src/plugins/data/public/search/legacy/call_client.ts +++ b/src/plugins/data/common/search/search_source/legacy/call_client.ts @@ -18,10 +18,9 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ISearchOptions } from 'src/plugins/data/common'; -import { FetchHandlers } from '../fetch'; +import { FetchHandlers, SearchRequest } from '../fetch'; import { defaultSearchStrategy } from './default_search_strategy'; -import { SearchRequest } from '../index'; +import { ISearchOptions } from '../../index'; export function callClient( searchRequests: SearchRequest[], diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts b/src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts similarity index 67% rename from src/plugins/data/public/search/legacy/default_search_strategy.test.ts rename to src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts index ad59e5c6c9625..3badd456bd72a 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts +++ b/src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts @@ -17,51 +17,50 @@ * under the License. */ -import { HttpStart } from 'src/core/public'; -import { coreMock } from '../../../../../core/public/mocks'; -import { getCallMsearch } from './call_msearch'; import { defaultSearchStrategy } from './default_search_strategy'; import { LegacyFetchHandlers, SearchStrategySearchParams } from './types'; import { BehaviorSubject } from 'rxjs'; const { search } = defaultSearchStrategy; -const msearchMock = jest.fn().mockResolvedValue({ body: { responses: [] } }); - -describe('defaultSearchStrategy', function () { - describe('search', function () { +describe('defaultSearchStrategy', () => { + describe('search', () => { let searchArgs: MockedKeys; - let http: jest.Mocked; beforeEach(() => { - msearchMock.mockClear(); - - http = coreMock.createStart().http; - http.post.mockResolvedValue(msearchMock); - searchArgs = { searchRequests: [ { index: { title: 'foo' }, + body: {}, }, ], getConfig: jest.fn(), onResponse: (req, res) => res, legacy: { - callMsearch: getCallMsearch({ http }), + callMsearch: jest.fn().mockResolvedValue(undefined), loadingCount$: new BehaviorSubject(0) as any, } as jest.Mocked, }; }); - test('calls http.post with the correct arguments', async () => { + test('calls callMsearch with the correct arguments', async () => { await search({ ...searchArgs }); - expect(http.post.mock.calls).toMatchInlineSnapshot(` + expect(searchArgs.legacy.callMsearch.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/internal/_msearch", Object { - "body": "{\\"searches\\":[{\\"header\\":{\\"index\\":\\"foo\\"}}]}", + "body": Object { + "searches": Array [ + Object { + "body": Object {}, + "header": Object { + "index": "foo", + "preference": undefined, + }, + }, + ], + }, "signal": AbortSignal {}, }, ], diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.ts b/src/plugins/data/common/search/search_source/legacy/default_search_strategy.ts similarity index 100% rename from src/plugins/data/public/search/legacy/default_search_strategy.ts rename to src/plugins/data/common/search/search_source/legacy/default_search_strategy.ts diff --git a/src/plugins/data/public/search/legacy/fetch_soon.test.ts b/src/plugins/data/common/search/search_source/legacy/fetch_soon.test.ts similarity index 96% rename from src/plugins/data/public/search/legacy/fetch_soon.test.ts rename to src/plugins/data/common/search/search_source/legacy/fetch_soon.test.ts index 7243ab158009a..81117513917c0 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.test.ts +++ b/src/plugins/data/common/search/search_source/legacy/fetch_soon.test.ts @@ -17,12 +17,13 @@ * under the License. */ -import { fetchSoon } from './fetch_soon'; -import { callClient } from './call_client'; -import { FetchHandlers } from '../fetch/types'; -import { SearchRequest } from '../index'; import { SearchResponse } from 'elasticsearch'; -import { GetConfigFn, UI_SETTINGS, ISearchOptions } from '../../../common'; +import { UI_SETTINGS } from '../../../constants'; +import { GetConfigFn } from '../../../types'; +import { FetchHandlers, SearchRequest } from '../fetch'; +import { ISearchOptions } from '../../index'; +import { callClient } from './call_client'; +import { fetchSoon } from './fetch_soon'; function getConfigStub(config: any = {}): GetConfigFn { return (key) => config[key]; diff --git a/src/plugins/data/public/search/legacy/fetch_soon.ts b/src/plugins/data/common/search/search_source/legacy/fetch_soon.ts similarity index 95% rename from src/plugins/data/public/search/legacy/fetch_soon.ts rename to src/plugins/data/common/search/search_source/legacy/fetch_soon.ts index 1c0573aa895d7..01ffc3876f6af 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.ts +++ b/src/plugins/data/common/search/search_source/legacy/fetch_soon.ts @@ -18,10 +18,10 @@ */ import { SearchResponse } from 'elasticsearch'; +import { UI_SETTINGS } from '../../../constants'; +import { FetchHandlers, SearchRequest } from '../fetch'; +import { ISearchOptions } from '../../index'; import { callClient } from './call_client'; -import { FetchHandlers } from '../fetch/types'; -import { SearchRequest } from '../index'; -import { UI_SETTINGS, ISearchOptions } from '../../../common'; /** * This function introduces a slight delay in the request process to allow multiple requests to queue diff --git a/src/plugins/data/common/search/search_source/legacy/index.ts b/src/plugins/data/common/search/search_source/legacy/index.ts new file mode 100644 index 0000000000000..26587b09ffd9e --- /dev/null +++ b/src/plugins/data/common/search/search_source/legacy/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { fetchSoon } from './fetch_soon'; +export * from './types'; diff --git a/src/plugins/data/public/search/legacy/types.ts b/src/plugins/data/common/search/search_source/legacy/types.ts similarity index 95% rename from src/plugins/data/public/search/legacy/types.ts rename to src/plugins/data/common/search/search_source/legacy/types.ts index 740bc22a7485c..1a0a96a76a703 100644 --- a/src/plugins/data/public/search/legacy/types.ts +++ b/src/plugins/data/common/search/search_source/legacy/types.ts @@ -19,8 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import { SearchResponse } from 'elasticsearch'; -import { FetchHandlers } from '../fetch'; -import { SearchRequest } from '..'; +import { FetchHandlers, SearchRequest } from '../fetch'; // @internal export interface LegacyFetchHandlers { diff --git a/src/plugins/data/public/search/search_source/migrate_legacy_query.ts b/src/plugins/data/common/search/search_source/migrate_legacy_query.ts similarity index 96% rename from src/plugins/data/public/search/search_source/migrate_legacy_query.ts rename to src/plugins/data/common/search/search_source/migrate_legacy_query.ts index 8d9b50d5a66b2..f271280170166 100644 --- a/src/plugins/data/public/search/search_source/migrate_legacy_query.ts +++ b/src/plugins/data/common/search/search_source/migrate_legacy_query.ts @@ -18,7 +18,7 @@ */ import { has } from 'lodash'; -import { Query } from 'src/plugins/data/public'; +import { Query } from '../../query/types'; /** * Creates a standardized query object from old queries that were either strings or pure ES query DSL diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts similarity index 100% rename from src/plugins/data/public/search/search_source/mocks.ts rename to src/plugins/data/common/search/search_source/mocks.ts diff --git a/src/plugins/data/public/search/search_source/normalize_sort_request.test.ts b/src/plugins/data/common/search/search_source/normalize_sort_request.test.ts similarity index 98% rename from src/plugins/data/public/search/search_source/normalize_sort_request.test.ts rename to src/plugins/data/common/search/search_source/normalize_sort_request.test.ts index 10004b87ca690..1899efbf3598d 100644 --- a/src/plugins/data/public/search/search_source/normalize_sort_request.test.ts +++ b/src/plugins/data/common/search/search_source/normalize_sort_request.test.ts @@ -19,7 +19,7 @@ import { normalizeSortRequest } from './normalize_sort_request'; import { SortDirection } from './types'; -import { IIndexPattern } from '../..'; +import { IIndexPattern } from '../../index_patterns'; describe('SearchSource#normalizeSortRequest', function () { const scriptedField = { diff --git a/src/plugins/data/public/search/search_source/normalize_sort_request.ts b/src/plugins/data/common/search/search_source/normalize_sort_request.ts similarity index 98% rename from src/plugins/data/public/search/search_source/normalize_sort_request.ts rename to src/plugins/data/common/search/search_source/normalize_sort_request.ts index 3ec0a13282d3e..e41c4482df9c9 100644 --- a/src/plugins/data/public/search/search_source/normalize_sort_request.ts +++ b/src/plugins/data/common/search/search_source/normalize_sort_request.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IIndexPattern } from '../..'; +import { IIndexPattern } from '../../index_patterns'; import { EsQuerySortValue, SortOptions } from './types'; export function normalizeSortRequest( diff --git a/src/plugins/data/public/search/search_source/parse_json.ts b/src/plugins/data/common/search/search_source/parse_json.ts similarity index 100% rename from src/plugins/data/public/search/search_source/parse_json.ts rename to src/plugins/data/common/search/search_source/parse_json.ts diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts similarity index 97% rename from src/plugins/data/public/search/search_source/search_source.test.ts rename to src/plugins/data/common/search/search_source/search_source.test.ts index d9a9fb2f4fef3..74abd9238bc2b 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -18,12 +18,12 @@ */ import { Observable, BehaviorSubject } from 'rxjs'; -import { GetConfigFn } from 'src/plugins/data/common'; -import { SearchSource, SearchSourceDependencies } from './search_source'; -import { IndexPattern, SortDirection } from '../..'; -import { fetchSoon } from '../legacy'; +import { IndexPattern } from '../../index_patterns'; +import { GetConfigFn } from '../../types'; +import { fetchSoon } from './legacy'; +import { SearchSource, SearchSourceDependencies, SortDirection } from './'; -jest.mock('../legacy', () => ({ +jest.mock('./legacy', () => ({ fetchSoon: jest.fn().mockResolvedValue({}), })); diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts similarity index 99% rename from src/plugins/data/public/search/search_source/search_source.ts rename to src/plugins/data/common/search/search_source/search_source.ts index 4afee223454e4..d8a036ce970dd 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -75,9 +75,10 @@ import { map } from 'rxjs/operators'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; -import { IIndexPattern, ISearchGeneric } from '../..'; +import { IIndexPattern } from '../../index_patterns'; +import { ISearchGeneric } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; -import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from '../fetch'; +import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from './fetch'; import { getEsQueryConfig, @@ -87,7 +88,7 @@ import { ISearchOptions, } from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; -import { fetchSoon } from '../legacy'; +import { fetchSoon } from './legacy'; import { extractReferences } from './extract_references'; /** @internal */ diff --git a/src/plugins/data/public/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts similarity index 100% rename from src/plugins/data/public/search/search_source/types.ts rename to src/plugins/data/common/search/search_source/types.ts diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index 7600bd9db6094..0a299b57275f8 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -17,6 +17,22 @@ * under the License. */ +import { Observable } from 'rxjs'; +import { IEsSearchRequest, IEsSearchResponse, ISearchOptions } from '../../common/search'; + +export type ISearch = ( + request: IKibanaSearchRequest, + options?: ISearchOptions +) => Observable; + +export type ISearchGeneric = < + SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse +>( + request: SearchStrategyRequest, + options?: ISearchOptions +) => Observable; + export interface IKibanaSearchResponse { /** * Some responses may contain a unique id to identify the request this response came from. diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 7ce53a219fb44..db8d9dba4e0c7 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -64,6 +64,7 @@ import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; import { SavedObject as SavedObject_3 } from 'src/core/public'; +import { SavedObjectReference as SavedObjectReference_2 } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; @@ -565,7 +566,7 @@ export const esFilters: { type?: string | undefined; key?: string | undefined; params?: any; - value?: string | ((formatter?: import("../common").FilterValueFormatter | undefined) => string) | undefined; + value?: string | undefined; }; $state?: import("../common").FilterState | undefined; query?: any; @@ -651,13 +652,12 @@ export type ExistsFilter = Filter & { // @public (undocumented) export const expandShorthand: (sh: Record) => MappingObject; -// Warning: (ae-forgotten-export) The symbol "SavedObjectReference" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "extractReferences" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const extractSearchSourceReferences: (state: SearchSourceFields) => [SearchSourceFields & { indexRefName?: string; -}, SavedObjectReference[]]; +}, SavedObjectReference_2[]]; // Warning: (ae-missing-release-tag) "FieldFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -791,18 +791,11 @@ export interface FieldMappingSpec { // Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface Filter { - // Warning: (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts - // - // (undocumented) +export type Filter = { $state?: FilterState; - // Warning: (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts - // - // (undocumented) meta: FilterMeta; - // (undocumented) query?: any; -} +}; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "FilterBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1378,7 +1371,7 @@ export interface IndexPatternTypeMeta { // @public (undocumented) export const injectSearchSourceReferences: (searchSourceFields: SearchSourceFields & { indexRefName: string; -}, references: SavedObjectReference[]) => SearchSourceFields; +}, references: SavedObjectReference_2[]) => SearchSourceFields; // Warning: (ae-missing-release-tag) "InputTimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1645,14 +1638,12 @@ export function plugin(initializerContext: PluginInitializerContext; // Warning: (ae-missing-release-tag) "TimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface TimeRange { - // (undocumented) +export type TimeRange = { from: string; - // (undocumented) - mode?: 'absolute' | 'relative'; - // (undocumented) to: string; -} + mode?: 'absolute' | 'relative'; +}; // Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2197,6 +2185,8 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/exists_filter.ts:30:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/exists_filter.ts:31:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/match_all_filter.ts:28:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.test.ts b/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.test.ts index 1b2d476570902..996a7aaa27c31 100644 --- a/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/map_and_flatten_filters.test.ts @@ -25,7 +25,9 @@ describe('filter manager utilities', () => { let filters: unknown; function getDisplayName(filter: Filter) { - return typeof filter.meta.value === 'function' ? filter.meta.value() : filter.meta.value; + return typeof filter.meta.value === 'function' + ? (filter.meta.value as any)() + : filter.meta.value; } beforeEach(() => { diff --git a/src/plugins/data/public/query/filter_manager/lib/map_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/map_filter.test.ts index 35d2f2b7b294e..7b303ca4d5314 100644 --- a/src/plugins/data/public/query/filter_manager/lib/map_filter.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/map_filter.test.ts @@ -22,7 +22,9 @@ import { Filter } from '../../../../common'; describe('filter manager utilities', () => { function getDisplayName(filter: Filter) { - return typeof filter.meta.value === 'function' ? filter.meta.value() : filter.meta.value; + return typeof filter.meta.value === 'function' + ? (filter.meta.value as any)() + : filter.meta.value; } describe('mapFilter()', () => { diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 50fbb114b39fd..1021ef0f91d52 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -20,7 +20,6 @@ import { get, hasIn } from 'lodash'; import { i18n } from '@kbn/i18n'; import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public'; -import { calculateObjectHash } from '../../../../../plugins/kibana_utils/public'; import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; @@ -38,6 +37,7 @@ import { getRequestInspectorStats, getResponseInspectorStats, IAggConfigs, + ISearchSource, tabifyAggResponse, } from '../../../common/search'; @@ -48,7 +48,6 @@ import { getQueryService, getSearchService, } from '../../services'; -import { ISearchSource } from '../search_source'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { serializeAggConfig } from './utils'; @@ -60,7 +59,6 @@ export interface RequestHandlerParams { indexPattern?: IIndexPattern; query?: Query; filters?: Filter[]; - forceFetch: boolean; filterManager: FilterManager; uiState?: PersistedState; partialRows?: boolean; @@ -80,7 +78,6 @@ const handleCourierRequest = async ({ indexPattern, query, filters, - forceFetch, partialRows, metricsAtAllLevels, inspectorAdapters, @@ -137,46 +134,35 @@ const handleCourierRequest = async ({ requestSearchSource.setField('filter', filters); requestSearchSource.setField('query', query); - const reqBody = await requestSearchSource.getSearchRequestBody(); - - const queryHash = calculateObjectHash(reqBody); - // We only need to reexecute the query, if forceFetch was true or the hash of the request body has changed - // since the last request - const shouldQuery = forceFetch || (searchSource as any).lastQuery !== queryHash; - - if (shouldQuery) { - inspectorAdapters.requests.reset(); - const request = inspectorAdapters.requests.start( - i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { - defaultMessage: 'Data', + inspectorAdapters.requests.reset(); + const request = inspectorAdapters.requests.start( + i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { + defaultMessage: 'Data', + }), + { + description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', }), - { - description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { - defaultMessage: - 'This request queries Elasticsearch to fetch the data for the visualization.', - }), - } - ); - request.stats(getRequestInspectorStats(requestSearchSource)); - - try { - const response = await requestSearchSource.fetch({ abortSignal }); - - (searchSource as any).lastQuery = queryHash; - - request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); - - (searchSource as any).rawResponse = response; - } catch (e) { - // Log any error during request to the inspector - request.error({ json: e }); - throw e; - } finally { - // Add the request body no matter if things went fine or not - requestSearchSource.getSearchRequestBody().then((req: unknown) => { - request.json(req); - }); } + ); + request.stats(getRequestInspectorStats(requestSearchSource)); + + try { + const response = await requestSearchSource.fetch({ abortSignal }); + + request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); + + (searchSource as any).rawResponse = response; + } catch (e) { + // Log any error during request to the inspector + request.error({ json: e }); + throw e; + } finally { + // Add the request body no matter if things went fine or not + requestSearchSource.getSearchRequestBody().then((req: unknown) => { + request.json(req); + }); } // Note that rawResponse is not deeply cloned here, so downstream applications using courier @@ -207,19 +193,11 @@ const handleCourierRequest = async ({ : undefined, }; - const tabifyCacheHash = calculateObjectHash({ tabifyAggs: aggs, ...tabifyParams }); - // We only need to reexecute tabify, if either we did a new request or some input params to tabify changed - const shouldCalculateNewTabify = - shouldQuery || (searchSource as any).lastTabifyHash !== tabifyCacheHash; - - if (shouldCalculateNewTabify) { - (searchSource as any).lastTabifyHash = tabifyCacheHash; - (searchSource as any).tabifiedResponse = tabifyAggResponse( - aggs, - (searchSource as any).finalResponse, - tabifyParams - ); - } + (searchSource as any).tabifiedResponse = tabifyAggResponse( + aggs, + (searchSource as any).finalResponse, + tabifyParams + ); inspectorAdapters.data.setTabularLoader( () => @@ -294,7 +272,6 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ query: get(input, 'query', undefined) as any, filters: get(input, 'filters', undefined), timeFields: args.timeFields, - forceFetch: true, metricsAtAllLevels: args.metricsAtAllLevels, partialRows: args.partialRows, inspectorAdapters: inspectorAdapters as Adapters, diff --git a/src/plugins/data/public/search/fetch/index.ts b/src/plugins/data/public/search/fetch/index.ts index 4b8511edfc26f..340a795d37bfb 100644 --- a/src/plugins/data/public/search/fetch/index.ts +++ b/src/plugins/data/public/search/fetch/index.ts @@ -17,8 +17,4 @@ * under the License. */ -export * from './types'; -export { getSearchParams, getSearchParamsFromRequest, getPreference } from './get_search_params'; - -export { RequestFailure } from './request_error'; export { handleResponse } from './handle_response'; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index c1af9699acbb2..fc3d71936a859 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -19,34 +19,31 @@ export * from './expressions'; +export { ISearchSetup, ISearchStart, ISearchStartSearchSource, SearchEnhancements } from './types'; + export { + ES_SEARCH_STRATEGY, + EsQuerySortValue, + extractReferences as extractSearchSourceReferences, + getSearchParamsFromRequest, + IEsSearchRequest, + IEsSearchResponse, + IKibanaSearchRequest, + IKibanaSearchResponse, + injectReferences as injectSearchSourceReferences, ISearch, ISearchGeneric, - ISearchSetup, - ISearchStart, - ISearchStartSearchSource, - SearchEnhancements, -} from './types'; - -export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search'; - -export { getEsPreference } from './es_search'; - -export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; - -export { SearchError, getSearchParamsFromRequest, SearchRequest } from './fetch'; - -export { ISearchSource, + parseSearchSourceJSON, + SearchError, + SearchRequest, SearchSource, SearchSourceDependencies, SearchSourceFields, - EsQuerySortValue, SortDirection, - extractReferences as extractSearchSourceReferences, - injectReferences as injectSearchSourceReferences, - parseSearchSourceJSON, -} from './search_source'; +} from '../../common/search'; + +export { getEsPreference } from './es_search'; export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor'; export { RequestTimeoutError } from './request_timeout_error'; diff --git a/src/plugins/data/public/search/legacy/call_msearch.test.ts b/src/plugins/data/public/search/legacy/call_msearch.test.ts new file mode 100644 index 0000000000000..da39bf521fe3d --- /dev/null +++ b/src/plugins/data/public/search/legacy/call_msearch.test.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { HttpStart } from 'src/core/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { getCallMsearch } from './call_msearch'; + +describe('callMsearch', () => { + const msearchMock = jest.fn().mockResolvedValue({ body: { responses: [] } }); + let http: jest.Mocked; + + beforeEach(() => { + msearchMock.mockClear(); + http = coreMock.createStart().http; + http.post.mockResolvedValue(msearchMock); + }); + + test('calls http.post with the correct arguments', async () => { + const searches = [{ header: { index: 'foo' }, body: {} }]; + const callMsearch = getCallMsearch({ http }); + await callMsearch({ + body: { searches }, + signal: new AbortController().signal, + }); + + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/internal/_msearch", + Object { + "body": "{\\"searches\\":[{\\"header\\":{\\"index\\":\\"foo\\"},\\"body\\":{}}]}", + "signal": AbortSignal {}, + }, + ], + ] + `); + }); +}); diff --git a/src/plugins/data/public/search/legacy/call_msearch.ts b/src/plugins/data/public/search/legacy/call_msearch.ts index fd4f8a07919f8..6b2b9b4da020b 100644 --- a/src/plugins/data/public/search/legacy/call_msearch.ts +++ b/src/plugins/data/public/search/legacy/call_msearch.ts @@ -18,7 +18,7 @@ */ import { HttpStart } from 'src/core/public'; -import { LegacyFetchHandlers } from './types'; +import { LegacyFetchHandlers } from '../../../common/search/search_source'; /** * Wrapper for calling the internal msearch endpoint from the client. diff --git a/src/plugins/data/public/search/legacy/index.ts b/src/plugins/data/public/search/legacy/index.ts index 74e516f407e8c..08e5eab788e76 100644 --- a/src/plugins/data/public/search/legacy/index.ts +++ b/src/plugins/data/public/search/legacy/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { fetchSoon } from './fetch_soon'; +export * from './call_msearch'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index f4ed7d8b122b9..fdd6a90013413 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -19,9 +19,9 @@ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { ISearchSetup, ISearchStart } from './types'; -import { searchSourceMock, createSearchSourceMock } from './search_source/mocks'; +import { searchSourceMock, createSearchSourceMock } from '../../common/search/search_source/mocks'; -export * from './search_source/mocks'; +export * from '../../common/search/search_source/mocks'; function createSetupContract(): jest.Mocked { return { diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index c41e1f78ee74e..d8937ed30e401 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -22,12 +22,16 @@ import { BehaviorSubject } from 'rxjs'; import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; import { handleResponse } from './fetch'; -import { getCallMsearch } from './legacy/call_msearch'; -import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source'; +import { + createSearchSource, + ISearchGeneric, + SearchSource, + SearchSourceDependencies, +} from '../../common/search'; +import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; import { IndexPatternsContract } from '../index_patterns/index_patterns'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; -import { ISearchGeneric } from './types'; import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; import { esdsl, esRawResponse } from './expressions'; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 83a542269046f..6ae5d83499aa6 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -17,38 +17,18 @@ * under the License. */ -import { Observable } from 'rxjs'; import { PackageInfo } from 'kibana/server'; import { ISearchInterceptor } from './search_interceptor'; -import { ISearchSource, SearchSourceFields } from './search_source'; import { SearchUsageCollector } from './collectors'; import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - IEsSearchRequest, - IEsSearchResponse, - ISearchOptions, -} from '../../common/search'; +import { ISearchGeneric, ISearchSource, SearchSourceFields } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; import { UsageCollectionSetup } from '../../../usage_collection/public'; -export type ISearch = ( - request: IKibanaSearchRequest, - options?: ISearchOptions -) => Observable; - -export type ISearchGeneric = < - SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, - SearchStrategyResponse extends IEsSearchResponse = IEsSearchResponse ->( - request: SearchStrategyRequest, - options?: ISearchOptions -) => Observable; - export interface SearchEnhancements { searchInterceptor: ISearchInterceptor; } + /** * The setup contract exposed by the Search plugin exposes the search strategy extension * point. diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 66fe40ff36a44..2024e9e7f2974 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -440,18 +440,11 @@ export type FieldFormatsGetConfigFn = GetConfigFn; // Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface Filter { - // Warning: (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts - // - // (undocumented) +export type Filter = { $state?: FilterState; - // Warning: (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts - // - // (undocumented) meta: FilterMeta; - // (undocumented) query?: any; -} +}; // Warning: (ae-forgotten-export) The symbol "IUiSettingsClient" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getDefaultSearchParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -944,14 +937,12 @@ export interface PluginStart { // Warning: (ae-missing-release-tag) "Query" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface Query { - // (undocumented) - language: string; - // (undocumented) +export type Query = { query: string | { [key: string]: any; }; -} + language: string; +}; // Warning: (ae-missing-release-tag) "RefreshInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1066,14 +1057,11 @@ export interface TabbedTable { // Warning: (ae-missing-release-tag) "TimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface TimeRange { - // (undocumented) +export type TimeRange = { from: string; - // (undocumented) - mode?: 'absolute' | 'relative'; - // (undocumented) to: string; -} + mode?: 'absolute' | 'relative'; +}; // Warning: (ae-missing-release-tag) "toSnakeCase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1123,6 +1111,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // Warnings were encountered during analysis: // +// src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/fields/types.ts:41:25 - (ae-forgotten-export) The symbol "IndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/embeddable/.eslintrc.json b/src/plugins/embeddable/.eslintrc.json new file mode 100644 index 0000000000000..2aab6c2d9093b --- /dev/null +++ b/src/plugins/embeddable/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/consistent-type-definitions": 0 + } +} diff --git a/src/plugins/embeddable/common/lib/migrate_base_input.ts b/src/plugins/embeddable/common/lib/migrate_base_input.ts new file mode 100644 index 0000000000000..0d5dc508e20ad --- /dev/null +++ b/src/plugins/embeddable/common/lib/migrate_base_input.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectReference } from '../../../../core/types'; +import { EmbeddableInput } from '../types'; + +export const telemetryBaseEmbeddableInput = ( + state: EmbeddableInput, + telemetryData: Record +) => { + return telemetryData; +}; + +export const extractBaseEmbeddableInput = (state: EmbeddableInput) => { + return { state, references: [] as SavedObjectReference[] }; +}; + +export const injectBaseEmbeddableInput = ( + state: EmbeddableInput, + references: SavedObjectReference[] +) => { + return state; +}; diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts new file mode 100644 index 0000000000000..68b842c934de8 --- /dev/null +++ b/src/plugins/embeddable/common/types.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SerializableState } from '../../kibana_utils/common'; +import { Query, TimeRange } from '../../data/common/query'; +import { Filter } from '../../data/common/es_query/filters'; + +export enum ViewMode { + EDIT = 'edit', + VIEW = 'view', +} + +export type EmbeddableInput = { + viewMode?: ViewMode; + title?: string; + /** + * Note this is not a saved object id. It is used to uniquely identify this + * Embeddable instance from others (e.g. inside a container). It's possible to + * have two Embeddables where everything else is the same but the id. + */ + id: string; + lastReloadRequestTime?: number; + hidePanelTitles?: boolean; + + /** + * Reserved key for enhancements added by other plugins. + */ + enhancements?: SerializableState; + + /** + * List of action IDs that this embeddable should not render. + */ + disabledActions?: string[]; + + /** + * Whether this embeddable should not execute triggers. + */ + disableTriggers?: boolean; + + /** + * Time range of the chart. + */ + timeRange?: TimeRange; + + /** + * Visualization query string used to narrow down results. + */ + query?: Query; + + /** + * Visualization filters used to narrow down results. + */ + filters?: Filter[]; +}; diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 6a8e6079232aa..1ecf76dbbd5c2 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -1,7 +1,7 @@ { "id": "embeddable", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": [ "inspector", diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 57253c1f741ab..c5d8853ada5e8 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -78,6 +78,8 @@ export { EmbeddableRendererProps, } from './lib'; +export { EnhancementRegistryDefinition } from './types'; + export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts index b22f16c94aff8..e2047dca1f770 100644 --- a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts +++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { SavedObjectAttributes } from 'kibana/public'; import { EmbeddableFactoryDefinition } from './embeddable_factory_definition'; import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; @@ -47,6 +48,9 @@ export const defaultEmbeddableFactoryProvider = < isEditable: def.isEditable.bind(def), getDisplayName: def.getDisplayName.bind(def), savedObjectMetaData: def.savedObjectMetaData, + telemetry: def.telemetry || (() => ({})), + inject: def.inject || ((state: EmbeddableInput) => state), + extract: def.extract || ((state: EmbeddableInput) => ({ state, references: [] })), }; return factory; }; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index ffe8a5bf6e7dc..9267d600360cf 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -21,10 +21,11 @@ import { cloneDeep, isEqual } from 'lodash'; import * as Rx from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { RenderCompleteDispatcher } from '../../../../kibana_utils/public'; -import { Adapters, ViewMode } from '../types'; +import { Adapters } from '../types'; import { IContainer } from '../containers'; -import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; +import { EmbeddableOutput, IEmbeddable } from './i_embeddable'; import { TriggerContextMapping } from '../ui_actions'; +import { EmbeddableInput, ViewMode } from '../../../common/types'; function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) { return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 7949b6fb8ba27..a6fa46fbc4e3e 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -23,6 +23,7 @@ import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; import { ErrorEmbeddable } from './error_embeddable'; import { IContainer } from '../containers/i_container'; import { PropertySpec } from '../types'; +import { PersistableState } from '../../../../kibana_utils/common'; export interface EmbeddableInstanceConfiguration { id: string; @@ -44,7 +45,7 @@ export interface EmbeddableFactory< TEmbeddableOutput >, TSavedObjectAttributes extends SavedObjectAttributes = SavedObjectAttributes -> { +> extends PersistableState { // A unique identified for this factory, which will be used to map an embeddable spec to // a factory that can generate an instance of it. readonly type: string; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts index b8985f7311ea9..224a11a201b88 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_definition.ts @@ -40,5 +40,8 @@ export type EmbeddableFactoryDefinition< | 'savedObjectMetaData' | 'canCreateNew' | 'getDefaultInput' + | 'telemetry' + | 'extract' + | 'inject' > >; diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index e8aecdba0abc4..3843950c164c9 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -20,57 +20,15 @@ import { Observable } from 'rxjs'; import { Adapters } from '../types'; import { IContainer } from '../containers/i_container'; -import { ViewMode } from '../types'; import { TriggerContextMapping } from '../../../../ui_actions/public'; -import type { TimeRange, Query, Filter } from '../../../../data/common'; +import { EmbeddableInput } from '../../../common/types'; export interface EmbeddableError { name: string; message: string; } -export interface EmbeddableInput { - viewMode?: ViewMode; - title?: string; - /** - * Note this is not a saved object id. It is used to uniquely identify this - * Embeddable instance from others (e.g. inside a container). It's possible to - * have two Embeddables where everything else is the same but the id. - */ - id: string; - lastReloadRequestTime?: number; - hidePanelTitles?: boolean; - - /** - * Reserved key for enhancements added by other plugins. - */ - enhancements?: unknown; - - /** - * List of action IDs that this embeddable should not render. - */ - disabledActions?: string[]; - - /** - * Whether this embeddable should not execute triggers. - */ - disableTriggers?: boolean; - - /** - * Time range of the chart. - */ - timeRange?: TimeRange; - - /** - * Visualization query string used to narrow down results. - */ - query?: Query; - - /** - * Visualization filters used to narrow down results. - */ - filters?: Filter[]; -} +export { EmbeddableInput }; export interface EmbeddableOutput { // Whether the embeddable is actively loading. diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container.tsx index ceaa74218904d..db71b94ac855f 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container.tsx @@ -32,7 +32,6 @@ export interface FilterableContainerInput extends ContainerInput { * https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type * here instead */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type InheritedChildrenInput = { filters: Filter[]; id?: string; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx index 913c3a0b30826..d47979b9419f3 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx @@ -30,7 +30,6 @@ export const HELLO_WORLD_CONTAINER = 'HELLO_WORLD_CONTAINER'; * https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type * here instead */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions type InheritedInput = { id: string; viewMode: ViewMode; diff --git a/src/plugins/embeddable/public/lib/types.ts b/src/plugins/embeddable/public/lib/types.ts index 1cfff7baca186..7fe189dea2381 100644 --- a/src/plugins/embeddable/public/lib/types.ts +++ b/src/plugins/embeddable/public/lib/types.ts @@ -32,10 +32,5 @@ export interface PropertySpec { description: string; value?: string; } - -export enum ViewMode { - EDIT = 'edit', - VIEW = 'view', -} - +export { ViewMode } from '../../common/types'; export { Adapters }; diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 2064236e9ae7f..26c10121adb3d 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -109,6 +109,7 @@ export const mockRefOrValEmbeddable = < const createSetupContract = (): Setup => { const setupContract: Setup = { registerEmbeddableFactory: jest.fn(), + registerEnhancement: jest.fn(), setCustomEmbeddableFactoryProvider: jest.fn(), }; return setupContract; @@ -118,6 +119,9 @@ const createStartContract = (): Start => { const startContract: Start = { getEmbeddableFactories: jest.fn(), getEmbeddableFactory: jest.fn(), + telemetry: jest.fn(), + extract: jest.fn(), + inject: jest.fn(), EmbeddablePanel: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), diff --git a/src/plugins/embeddable/public/plugin.test.ts b/src/plugins/embeddable/public/plugin.test.ts index e37d602ad8cac..5fd3bcdd61318 100644 --- a/src/plugins/embeddable/public/plugin.test.ts +++ b/src/plugins/embeddable/public/plugin.test.ts @@ -22,21 +22,6 @@ import { EmbeddableFactoryProvider } from './types'; import { defaultEmbeddableFactoryProvider } from './lib'; import { HelloWorldEmbeddable } from '../../../../examples/embeddable_examples/public'; -test('cannot register embeddable factory with the same ID', async () => { - const coreSetup = coreMock.createSetup(); - const coreStart = coreMock.createStart(); - const { setup } = testPlugin(coreSetup, coreStart); - const embeddableFactoryId = 'ID'; - const embeddableFactory = {} as any; - - setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory); - expect(() => - setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory) - ).toThrowError( - 'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.' - ); -}); - test('can set custom embeddable factory provider', async () => { const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); @@ -108,3 +93,90 @@ test('custom embeddable factory provider test for intercepting embeddable creati await new Promise((resolve) => process.nextTick(resolve)); expect(updateCount).toEqual(0); }); + +describe('embeddable factory', () => { + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const { setup, doStart } = testPlugin(coreSetup, coreStart); + const start = doStart(); + const embeddableFactoryId = 'ID'; + const embeddableFactory = { + type: embeddableFactoryId, + create: jest.fn(), + getDisplayName: () => 'Test', + isEditable: () => Promise.resolve(true), + extract: jest.fn().mockImplementation((state) => ({ state, references: [] })), + inject: jest.fn().mockImplementation((state) => state), + telemetry: jest.fn().mockResolvedValue({}), + } as any; + const embeddableState = { + id: embeddableFactoryId, + my: 'state', + } as any; + + test('cannot register embeddable factory with the same ID', async () => { + setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory); + expect(() => + setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory) + ).toThrowError( + 'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.' + ); + }); + + test('embeddableFactory extract function gets called when calling embeddable extract', () => { + start.extract(embeddableState); + expect(embeddableFactory.extract).toBeCalledWith(embeddableState); + }); + + test('embeddableFactory inject function gets called when calling embeddable inject', () => { + start.inject(embeddableState, []); + expect(embeddableFactory.extract).toBeCalledWith(embeddableState); + }); + + test('embeddableFactory telemetry function gets called when calling embeddable telemetry', () => { + start.telemetry(embeddableState, {}); + expect(embeddableFactory.telemetry).toBeCalledWith(embeddableState, {}); + }); +}); + +describe('embeddable enhancements', () => { + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const { setup, doStart } = testPlugin(coreSetup, coreStart); + const start = doStart(); + const embeddableEnhancement = { + id: 'test', + extract: jest.fn().mockImplementation((state) => ({ state, references: [] })), + inject: jest.fn().mockImplementation((state) => state), + telemetry: jest.fn().mockResolvedValue({}), + } as any; + const embeddableState = { + enhancements: { + test: { + my: 'state', + }, + }, + } as any; + + test('cannot register embeddable enhancement with the same ID', async () => { + setup.registerEnhancement(embeddableEnhancement); + expect(() => setup.registerEnhancement(embeddableEnhancement)).toThrowError( + 'enhancement with id test already exists in the registry' + ); + }); + + test('enhancement extract function gets called when calling embeddable extract', () => { + start.extract(embeddableState); + expect(embeddableEnhancement.extract).toBeCalledWith(embeddableState.enhancements.test); + }); + + test('enhancement inject function gets called when calling embeddable inject', () => { + start.inject(embeddableState, []); + expect(embeddableEnhancement.extract).toBeCalledWith(embeddableState.enhancements.test); + }); + + test('enhancement telemetry function gets called when calling embeddable telemetry', () => { + start.telemetry(embeddableState, {}); + expect(embeddableEnhancement.telemetry).toBeCalledWith(embeddableState.enhancements.test, {}); + }); +}); diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 2ca31994b722d..00eb923c26662 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -18,6 +18,7 @@ */ import React from 'react'; import { Subscription } from 'rxjs'; +import { identity } from 'lodash'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { getSavedObjectFinder } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; @@ -29,8 +30,15 @@ import { Plugin, ScopedHistory, PublicAppInfo, + SavedObjectReference, } from '../../../core/public'; -import { EmbeddableFactoryRegistry, EmbeddableFactoryProvider } from './types'; +import { + EmbeddableFactoryRegistry, + EmbeddableFactoryProvider, + EnhancementsRegistry, + EnhancementRegistryDefinition, + EnhancementRegistryItem, +} from './types'; import { bootstrap } from './bootstrap'; import { EmbeddableFactory, @@ -42,6 +50,12 @@ import { } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; +import { + extractBaseEmbeddableInput, + injectBaseEmbeddableInput, + telemetryBaseEmbeddableInput, +} from '../common/lib/migrate_base_input'; +import { PersistableState, SerializableState } from '../../kibana_utils/common'; export interface EmbeddableSetupDependencies { data: DataPublicPluginSetup; @@ -63,10 +77,11 @@ export interface EmbeddableSetup { id: string, factory: EmbeddableFactoryDefinition ) => () => EmbeddableFactory; + registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; setCustomEmbeddableFactoryProvider: (customProvider: EmbeddableFactoryProvider) => void; } -export interface EmbeddableStart { +export interface EmbeddableStart extends PersistableState { getEmbeddableFactory: < I extends EmbeddableInput = EmbeddableInput, O extends EmbeddableOutput = EmbeddableOutput, @@ -88,6 +103,7 @@ export class EmbeddablePublicPlugin implements Plugin = new Map(); private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); + private readonly enhancements: EnhancementsRegistry = new Map(); private customEmbeddableFactoryProvider?: EmbeddableFactoryProvider; private outgoingOnlyStateTransfer: EmbeddableStateTransfer = {} as EmbeddableStateTransfer; private isRegistryReady = false; @@ -101,6 +117,7 @@ export class EmbeddablePublicPlugin implements Plugin { if (this.customEmbeddableFactoryProvider) { throw new Error( @@ -168,6 +185,9 @@ export class EmbeddablePublicPlugin implements Plugin = {}) => { + const enhancements: Record = state.enhancements || {}; + const factory = this.getEmbeddableFactory(state.id); + + let telemetry = telemetryBaseEmbeddableInput(state, telemetryData); + if (factory) { + telemetry = factory.telemetry(state, telemetry); + } + Object.keys(enhancements).map((key) => { + if (!enhancements[key]) return; + telemetry = this.getEnhancement(key).telemetry(enhancements[key], telemetry); + }); + + return telemetry; + }; + + private extract = (state: EmbeddableInput) => { + const enhancements = state.enhancements || {}; + const factory = this.getEmbeddableFactory(state.id); + + const baseResponse = extractBaseEmbeddableInput(state); + let updatedInput = baseResponse.state; + const refs = baseResponse.references; + + if (factory) { + const factoryResponse = factory.extract(state); + updatedInput = factoryResponse.state; + refs.push(...factoryResponse.references); + } + + updatedInput.enhancements = {}; + Object.keys(enhancements).forEach((key) => { + if (!enhancements[key]) return; + const enhancementResult = this.getEnhancement(key).extract( + enhancements[key] as SerializableState + ); + refs.push(...enhancementResult.references); + updatedInput.enhancements![key] = enhancementResult.state; + }); + + return { + state: updatedInput, + references: refs, + }; + }; + + private inject = (state: EmbeddableInput, references: SavedObjectReference[]) => { + const enhancements = state.enhancements || {}; + const factory = this.getEmbeddableFactory(state.id); + + let updatedInput = injectBaseEmbeddableInput(state, references); + + if (factory) { + updatedInput = factory.inject(updatedInput, references); + } + + updatedInput.enhancements = {}; + Object.keys(enhancements).forEach((key) => { + if (!enhancements[key]) return; + updatedInput.enhancements![key] = this.getEnhancement(key).inject( + enhancements[key] as SerializableState, + references + ); + }); + + return updatedInput; + }; + + private registerEnhancement = (enhancement: EnhancementRegistryDefinition) => { + if (this.enhancements.has(enhancement.id)) { + throw new Error(`enhancement with id ${enhancement.id} already exists in the registry`); + } + this.enhancements.set(enhancement.id, { + id: enhancement.id, + telemetry: enhancement.telemetry || (() => ({})), + inject: enhancement.inject || identity, + extract: + enhancement.extract || + ((state: SerializableState) => { + return { state, references: [] }; + }), + }); + }; + + private getEnhancement = (id: string): EnhancementRegistryItem => { + return ( + this.enhancements.get(id) || { + id: 'unknown', + telemetry: () => ({}), + inject: identity, + extract: (state: SerializableState) => { + return { state, references: [] }; + }, + } + ); + }; + private getEmbeddableFactories = () => { this.ensureFactoriesExist(); return this.embeddableFactories.values(); @@ -215,12 +332,6 @@ export class EmbeddablePublicPlugin implements Plugin; }; diff --git a/src/plugins/embeddable/public/types.ts b/src/plugins/embeddable/public/types.ts index 2d112b2359818..c5148bbaefb6b 100644 --- a/src/plugins/embeddable/public/types.ts +++ b/src/plugins/embeddable/public/types.ts @@ -25,8 +25,24 @@ import { IEmbeddable, EmbeddableFactoryDefinition, } from './lib/embeddables'; +import { + PersistableState, + PersistableStateDefinition, + SerializableState, +} from '../../kibana_utils/common'; export type EmbeddableFactoryRegistry = Map; +export type EnhancementsRegistry = Map; + +export interface EnhancementRegistryDefinition

+ extends PersistableStateDefinition

{ + id: string; +} + +export interface EnhancementRegistryItem

+ extends PersistableState

{ + id: string; +} export type EmbeddableFactoryProvider = < I extends EmbeddableInput = EmbeddableInput, diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js b/src/plugins/embeddable/server/index.ts similarity index 61% rename from src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js rename to src/plugins/embeddable/server/index.ts index 0b8786f0c2841..1138478bff4b7 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js +++ b/src/plugins/embeddable/server/index.ts @@ -17,18 +17,10 @@ * under the License. */ -import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; +import { EmbeddableServerPlugin, EmbeddableSetup } from './plugin'; -/* - * A simple utility for generating a handler that provides a signal to the handler that signals when - * the client has closed the connection on this request. - */ -export function abortableRequestHandler(fn) { - return (req, ...args) => { - const controller = new AbortController(); - req.events.once('disconnect', () => { - controller.abort(); - }); - return fn(controller.signal, req, ...args); - }; -} +export { EmbeddableSetup }; + +export { EnhancementRegistryDefinition, EmbeddableRegistryDefinition } from './types'; + +export const plugin = () => new EmbeddableServerPlugin(); diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts new file mode 100644 index 0000000000000..f79c4b7620110 --- /dev/null +++ b/src/plugins/embeddable/server/plugin.ts @@ -0,0 +1,186 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, CoreStart, Plugin, SavedObjectReference } from 'kibana/server'; +import { identity } from 'lodash'; +import { + EmbeddableFactoryRegistry, + EnhancementsRegistry, + EnhancementRegistryDefinition, + EnhancementRegistryItem, + EmbeddableRegistryDefinition, +} from './types'; +import { + extractBaseEmbeddableInput, + injectBaseEmbeddableInput, + telemetryBaseEmbeddableInput, +} from '../common/lib/migrate_base_input'; +import { SerializableState } from '../../kibana_utils/common'; +import { EmbeddableInput } from '../common/types'; + +export interface EmbeddableSetup { + registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; + registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; +} + +export class EmbeddableServerPlugin implements Plugin { + private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); + private readonly enhancements: EnhancementsRegistry = new Map(); + + public setup(core: CoreSetup) { + return { + registerEmbeddableFactory: this.registerEmbeddableFactory, + registerEnhancement: this.registerEnhancement, + }; + } + + public start(core: CoreStart) { + return { + telemetry: this.telemetry, + extract: this.extract, + inject: this.inject, + }; + } + + public stop() {} + + private telemetry = (state: EmbeddableInput, telemetryData: Record = {}) => { + const enhancements: Record = state.enhancements || {}; + const factory = this.getEmbeddableFactory(state.id); + + let telemetry = telemetryBaseEmbeddableInput(state, telemetryData); + if (factory) { + telemetry = factory.telemetry(state, telemetry); + } + Object.keys(enhancements).map((key) => { + if (!enhancements[key]) return; + telemetry = this.getEnhancement(key).telemetry(enhancements[key], telemetry); + }); + + return telemetry; + }; + + private extract = (state: EmbeddableInput) => { + const enhancements = state.enhancements || {}; + const factory = this.getEmbeddableFactory(state.id); + + const baseResponse = extractBaseEmbeddableInput(state); + let updatedInput = baseResponse.state; + const refs = baseResponse.references; + + if (factory) { + const factoryResponse = factory.extract(state); + updatedInput = factoryResponse.state; + refs.push(...factoryResponse.references); + } + + updatedInput.enhancements = {}; + Object.keys(enhancements).forEach((key) => { + if (!enhancements[key]) return; + const enhancementResult = this.getEnhancement(key).extract( + enhancements[key] as SerializableState + ); + refs.push(...enhancementResult.references); + updatedInput.enhancements![key] = enhancementResult.state; + }); + + return { + state: updatedInput, + references: refs, + }; + }; + + private inject = (state: EmbeddableInput, references: SavedObjectReference[]) => { + const enhancements = state.enhancements || {}; + const factory = this.getEmbeddableFactory(state.id); + + let updatedInput = injectBaseEmbeddableInput(state, references); + + if (factory) { + updatedInput = factory.inject(updatedInput, references); + } + + updatedInput.enhancements = {}; + Object.keys(enhancements).forEach((key) => { + if (!enhancements[key]) return; + updatedInput.enhancements![key] = this.getEnhancement(key).inject( + enhancements[key] as SerializableState, + references + ); + }); + + return updatedInput; + }; + + private registerEnhancement = (enhancement: EnhancementRegistryDefinition) => { + if (this.enhancements.has(enhancement.id)) { + throw new Error(`enhancement with id ${enhancement.id} already exists in the registry`); + } + this.enhancements.set(enhancement.id, { + id: enhancement.id, + telemetry: enhancement.telemetry || (() => ({})), + inject: enhancement.inject || identity, + extract: + enhancement.extract || + ((state: SerializableState) => { + return { state, references: [] }; + }), + }); + }; + + private getEnhancement = (id: string): EnhancementRegistryItem => { + return ( + this.enhancements.get(id) || { + id: 'unknown', + telemetry: () => ({}), + inject: identity, + extract: (state: SerializableState) => { + return { state, references: [] }; + }, + } + ); + }; + + private registerEmbeddableFactory = (factory: EmbeddableRegistryDefinition) => { + if (this.embeddableFactories.has(factory.id)) { + throw new Error( + `Embeddable factory [embeddableFactoryId = ${factory.id}] already registered in Embeddables API.` + ); + } + this.embeddableFactories.set(factory.id, { + id: factory.id, + telemetry: factory.telemetry || (() => ({})), + inject: factory.inject || identity, + extract: factory.extract || ((state: EmbeddableInput) => ({ state, references: [] })), + }); + }; + + private getEmbeddableFactory = (embeddableFactoryId: string) => { + return ( + this.embeddableFactories.get(embeddableFactoryId) || { + id: 'unknown', + telemetry: () => ({}), + inject: (state: EmbeddableInput) => state, + extract: (state: EmbeddableInput) => { + return { state, references: [] }; + }, + } + ); + }; +} diff --git a/src/plugins/embeddable/server/types.ts b/src/plugins/embeddable/server/types.ts new file mode 100644 index 0000000000000..64f9325dad3cb --- /dev/null +++ b/src/plugins/embeddable/server/types.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + PersistableState, + PersistableStateDefinition, + SerializableState, +} from '../../kibana_utils/common'; +import { EmbeddableInput } from '../common/types'; + +export type EmbeddableFactoryRegistry = Map; +export type EnhancementsRegistry = Map; + +export interface EnhancementRegistryDefinition

+ extends PersistableStateDefinition

{ + id: string; +} + +export interface EnhancementRegistryItem

+ extends PersistableState

{ + id: string; +} + +export interface EmbeddableRegistryDefinition

+ extends PersistableStateDefinition

{ + id: string; +} + +export interface EmbeddableRegistryItem

+ extends PersistableState

{ + id: string; +} diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap index 1b10756c2975c..bf1e8c8f0b401 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap @@ -164,6 +164,7 @@ exports[`home directories should not render directory entry when showOnHomePage {stackManagement ? ( - + `; + +exports[`ManageData render empty without any features 1`] = ``; diff --git a/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx b/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx index 5d00370caf2cc..0e86bf7dd3d84 100644 --- a/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx +++ b/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx @@ -88,4 +88,9 @@ describe('ManageData', () => { ); expect(component).toMatchSnapshot(); }); + + test('render empty without any features', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/plugins/home/public/application/components/manage_data/manage_data.tsx b/src/plugins/home/public/application/components/manage_data/manage_data.tsx index 0dfb4f949f0c7..85f1bc04f353b 100644 --- a/src/plugins/home/public/application/components/manage_data/manage_data.tsx +++ b/src/plugins/home/public/application/components/manage_data/manage_data.tsx @@ -36,31 +36,37 @@ export const ManageData: FC = ({ addBasePath, features }) => ( <> {features.length > 1 &&

- -

- -

-
+ {features.length > 0 && ( +
+ +

+ +

+
- + - - {features.map((feature) => ( - - - - ))} - -
+ + {features.map((feature) => ( + + + + ))} + +
+ )} ); diff --git a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap index 4e8441bd64b11..ad92aac67d51b 100644 --- a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap +++ b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap @@ -3,6 +3,7 @@ exports[`SolutionPanel renders the solution panel for the given solution 1`] = ` diff --git a/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx b/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx index c2ae2f82eaa46..83572e238bffd 100644 --- a/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx @@ -53,6 +53,7 @@ interface Props { export const SolutionPanel: FC = ({ addBasePath, solution }) => ( { expect(service.get()).toEqual([]); }); }); + + describe('visibility filtering', () => { + test('retains items with no "visible" callback', () => { + const service = new FeatureCatalogueRegistry(); + service.setup().register(DASHBOARD_FEATURE); + const capabilities = { catalogue: {} } as any; + service.start({ capabilities }); + expect(service.get()).toEqual([DASHBOARD_FEATURE]); + }); + + test('retains items with a "visible" callback which returns "true"', () => { + const service = new FeatureCatalogueRegistry(); + const feature = { + ...DASHBOARD_FEATURE, + visible: () => true, + }; + service.setup().register(feature); + const capabilities = { catalogue: {} } as any; + service.start({ capabilities }); + expect(service.get()).toEqual([feature]); + }); + + test('removes items with a "visible" callback which returns "false"', () => { + const service = new FeatureCatalogueRegistry(); + const feature = { + ...DASHBOARD_FEATURE, + visible: () => false, + }; + service.setup().register(feature); + const capabilities = { catalogue: {} } as any; + service.start({ capabilities }); + expect(service.get()).toEqual([]); + }); + }); }); describe('title sorting', () => { diff --git a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts index 766afb11a87c0..d965042b65cef 100644 --- a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts +++ b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts @@ -45,6 +45,8 @@ export interface FeatureCatalogueEntry { readonly showOnHomePage: boolean; /** An ordinal used to sort features relative to one another for display on the home page */ readonly order?: number; + /** Optional function to control visibility of this feature. */ + readonly visible?: () => boolean; } /** @public */ @@ -103,7 +105,10 @@ export class FeatureCatalogueRegistry { } const capabilities = this.capabilities; return [...this.features.values()] - .filter((entry) => capabilities.catalogue[entry.id] !== false) + .filter( + (entry) => + capabilities.catalogue[entry.id] !== false && (entry.visible ? entry.visible() : true) + ) .sort(compareByKey('title')); } diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 1ec5737c5a38b..e09290c811c7b 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -29,3 +29,4 @@ export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_w export { url } from './url'; export { now } from './now'; export { calculateObjectHash } from './calculate_object_hash'; +export * from './persistable_state'; diff --git a/src/plugins/kibana_utils/common/persistable_state/index.ts b/src/plugins/kibana_utils/common/persistable_state/index.ts new file mode 100644 index 0000000000000..ae5e3d514554c --- /dev/null +++ b/src/plugins/kibana_utils/common/persistable_state/index.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectReference } from '../../../../core/types'; + +export type SerializableValue = string | number | boolean | null | undefined | SerializableState; +export type Serializable = SerializableValue | SerializableValue[]; + +// eslint-disable-next-line +export type SerializableState = { + [key: string]: Serializable; +}; + +export interface PersistableState

{ + /** + * function to extract telemetry information + * @param state + * @param collector + */ + telemetry: (state: P, collector: Record) => Record; + /** + * inject function receives state and a list of references and should return state with references injected + * default is identity function + * @param state + * @param references + */ + inject: (state: P, references: SavedObjectReference[]) => P; + /** + * extract function receives state and should return state with references extracted and array of references + * default returns same state with empty reference array + * @param state + */ + extract: (state: P) => { state: P; references: SavedObjectReference[] }; +} + +export type PersistableStateDefinition

= Partial< + PersistableState

+>; diff --git a/src/plugins/kibana_utils/public/ui/configurable.ts b/src/plugins/kibana_utils/public/ui/configurable.ts index 3fa5cdc8b5e47..89bce5ae423ee 100644 --- a/src/plugins/kibana_utils/public/ui/configurable.ts +++ b/src/plugins/kibana_utils/public/ui/configurable.ts @@ -18,11 +18,15 @@ */ import { UiComponent } from '../../common/ui/ui_component'; +import { SerializableState } from '../../common'; /** * Represents something that can be configured by user using UI. */ -export interface Configurable { +export interface Configurable< + Config extends SerializableState = SerializableState, + Context = object +> { /** * Create default config for this item, used when item is created for the first time. */ @@ -42,7 +46,10 @@ export interface Configurable /** * Props provided to `CollectConfig` component on every re-render. */ -export interface CollectConfigProps { +export interface CollectConfigProps< + Config extends SerializableState = SerializableState, + Context = object +> { /** * Current (latest) config of the item. */ diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index fafedf46c2bda..808578c470ae1 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -47,6 +47,8 @@ export class ManagementPlugin implements Plugin(() => ({})); + private hasAnyEnabledApps = true; + constructor(private initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, { home }: ManagementSetupDependencies) { @@ -65,6 +67,7 @@ export class ManagementPlugin implements Plugin this.hasAnyEnabledApps, }); } @@ -96,11 +99,11 @@ export class ManagementPlugin implements Plugin section.getAppsEnabled().length > 0); - if (!hasAnyEnabledApps) { + if (!this.hasAnyEnabledApps) { this.appUpdater.next(() => { return { status: AppStatus.inaccessible, diff --git a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts index 0cf6f3723a639..3442f84599fb8 100644 --- a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts +++ b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts @@ -78,7 +78,6 @@ export function getTimelionRequestHandler({ filters: Filter[]; query: Query; visParams: VisParams; - forceFetch?: boolean; }): Promise { const expression = visParams.expression; diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts index 7be18a4774d94..d3c6ca5d90371 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts +++ b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts @@ -76,7 +76,6 @@ export const getTimelionVisualizationConfig = ( query: get(input, 'query') as Query, filters: get(input, 'filters') as Filter[], visParams, - forceFetch: true, }); response.visType = TIMELION_VIS_NAME; diff --git a/src/plugins/visualizations/public/expressions/visualization_function.ts b/src/plugins/visualizations/public/expressions/visualization_function.ts index 68a153f4272a3..f4241808940b2 100644 --- a/src/plugins/visualizations/public/expressions/visualization_function.ts +++ b/src/plugins/visualizations/public/expressions/visualization_function.ts @@ -117,7 +117,6 @@ export const visualization = (): ExpressionFunctionVisualization => ({ uiState, inspectorAdapters, queryFilter: getFilterManager(), - forceFetch: true, aggs, }); } diff --git a/src/plugins/visualizations/public/vis.test.ts b/src/plugins/visualizations/public/vis.test.ts index a0da8d83bed51..c271888b7c7a4 100644 --- a/src/plugins/visualizations/public/vis.test.ts +++ b/src/plugins/visualizations/public/vis.test.ts @@ -35,7 +35,7 @@ jest.mock('./services', () => { // eslint-disable-next-line const { BaseVisType } = require('./vis_types/base_vis_type'); // eslint-disable-next-line - const { SearchSource } = require('../../data/public/search/search_source'); + const { SearchSource } = require('../../data/common/search/search_source'); // eslint-disable-next-line const fixturesStubbedLogstashIndexPatternProvider = require('../../../fixtures/stubbed_logstash_index_pattern'); const visType = new BaseVisType({ diff --git a/test/api_integration/apis/elasticsearch/index.js b/test/api_integration/apis/elasticsearch/index.js deleted file mode 100644 index 5a3dc47aab9bb..0000000000000 --- a/test/api_integration/apis/elasticsearch/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('elasticsearch', () => { - before(() => esArchiver.load('elasticsearch')); - after(() => esArchiver.unload('elasticsearch')); - - it('allows search to specific index', async () => - await supertest.post('/elasticsearch/elasticsearch/_search').expect(200)); - - it('allows msearch', async () => - await supertest - .post('/elasticsearch/_msearch') - .set('content-type', 'application/x-ndjson') - .send( - '{"index":"logstash-2015.04.21","ignore_unavailable":true}\n{"size":500,"sort":{"@timestamp":"desc"},"query":{"bool":{"must":[{"query_string":{"analyze_wildcard":true,"query":"*"}},{"bool":{"must":[{"range":{"@timestamp":{"gte":1429577068175,"lte":1429577968175}}}],"must_not":[]}}],"must_not":[]}},"highlight":{"pre_tags":["@kibana-highlighted-field@"],"post_tags":["@/kibana-highlighted-field@"],"fields":{"*":{}}},"aggs":{"2":{"date_histogram":{"field":"@timestamp","interval":"30s","min_doc_count":0,"extended_bounds":{"min":1429577068175,"max":1429577968175}}}},"stored_fields":["*"],"_source": true,"script_fields":{},"docvalue_fields":["timestamp_offset","@timestamp","utc_time"]}\n' - ) - .expect(200)); - }); -} diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index bfbf873cf0616..d07c099634005 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -20,7 +20,6 @@ export default function ({ loadTestFile }) { describe('apis', () => { loadTestFile(require.resolve('./core')); - loadTestFile(require.resolve('./elasticsearch')); loadTestFile(require.resolve('./general')); loadTestFile(require.resolve('./home')); loadTestFile(require.resolve('./index_patterns')); diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index e8f8982d7163c..c12c633926c1c 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -43,6 +43,14 @@ export function HomePageProvider({ getService, getPageObjects }: FtrProviderCont return !(await testSubjects.exists(`addSampleDataSet${id}`)); } + async getVisibileSolutions() { + const solutionPanels = await testSubjects.findAll('~homSolutionPanel', 2000); + const panelAttributes = await Promise.all( + solutionPanels.map((panel) => panel.getAttribute('data-test-subj')) + ); + return panelAttributes.map((attributeValue) => attributeValue.split('homSolutionPanel_')[1]); + } + async addSampleDataSet(id: string) { const isInstalled = await this.isSampleDataSetInstalled(id); if (!isInstalled) { diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index ace6a48ed8ff5..87a1bc20920a4 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "28.4.0", + "@elastic/eui": "29.0.0", "@kbn/plugin-helpers": "1.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json index d98fa468bd6d1..8bbf6274bd15f 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "28.4.0", + "@elastic/eui": "29.0.0", "react": "^16.12.0", "typescript": "4.0.2" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 3ac03b444deaf..c0d9a03d02c32 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "28.4.0", + "@elastic/eui": "29.0.0", "@kbn/plugin-helpers": "1.0.0", "react": "^16.12.0", "typescript": "4.0.2" diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index bdd0fbea35fa8..a700781438706 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -49,7 +49,7 @@ "xpack.server": "legacy/server", "xpack.securitySolution": "plugins/security_solution", "xpack.snapshotRestore": "plugins/snapshot_restore", - "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], + "xpack.spaces": "plugins/spaces", "xpack.taskManager": "legacy/plugins/task_manager", "xpack.transform": "plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index a693e008db6ea..e6f160ce8c654 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -18,7 +18,6 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`, '^src/plugins/(.*)': `${kibanaDirectory}/src/plugins/$1`, - '^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`, '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, '\\.module.(css|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/css_module_mock.js`, diff --git a/x-pack/examples/ui_actions_enhanced_examples/.eslintrc.json b/x-pack/examples/ui_actions_enhanced_examples/.eslintrc.json new file mode 100644 index 0000000000000..2aab6c2d9093b --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/consistent-type-definitions": 0 + } +} diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx index fd782f5468c85..cac5f0b29dc6e 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx @@ -17,9 +17,9 @@ import { export type ActionContext = ChartActionContext; -export interface Config { +export type Config = { name: string; -} +}; const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN = 'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN'; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_only_range_select_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_only_range_select_drilldown/index.tsx index 7394690a61eae..fa2f0825f9335 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_only_range_select_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_only_range_select_drilldown/index.tsx @@ -13,9 +13,9 @@ import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/publ import { SELECT_RANGE_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; import { BaseActionFactoryContext } from '../../../../plugins/ui_actions_enhanced/public/dynamic_actions'; -export interface Config { +export type Config = { name: string; -} +}; const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT = 'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT'; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts index a10e8ad707e97..692de571e8a00 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts @@ -9,7 +9,7 @@ import { ApplyGlobalFilterActionContext } from '../../../../../src/plugins/data/ export type ActionContext = ApplyGlobalFilterActionContext; -export interface Config { +export type Config = { /** * Whether to use a user selected index pattern, stored in `indexPatternId` field. */ @@ -30,6 +30,6 @@ export interface Config { * Whether to carry over source dashboard time range. */ carryTimeRange: boolean; -} +}; export type CollectConfigProps = CollectConfigPropsBase; diff --git a/x-pack/index.js b/x-pack/index.js index 745b4bd72dde8..cb68004c26d65 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -5,8 +5,7 @@ */ import { xpackMain } from './legacy/plugins/xpack_main'; -import { spaces } from './legacy/plugins/spaces'; module.exports = function (kibana) { - return [xpackMain(kibana), spaces(kibana)]; + return [xpackMain(kibana)]; }; diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts deleted file mode 100644 index 725d022879e0d..0000000000000 --- a/x-pack/legacy/plugins/spaces/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { resolve } from 'path'; -import KbnServer, { Server } from 'src/legacy/server/kbn_server'; -import { Legacy } from 'kibana'; -import { KibanaRequest } from '../../../../src/core/server'; -import { SpacesPluginSetup } from '../../../plugins/spaces/server'; -import { wrapError } from './server/lib/errors'; - -export const spaces = (kibana: Record) => - new kibana.Plugin({ - id: 'spaces', - configPrefix: 'xpack.spaces', - publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch', 'xpack_main'], - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }) - .unknown() - .default(); - }, - uiExports: { - injectDefaultVars(server: Server) { - return { - serverBasePath: server.config().get('server.basePath'), - activeSpace: null, - }; - }, - async replaceInjectedVars( - vars: Record, - request: Legacy.Request, - server: Server - ) { - // NOTICE: use of `activeSpace` is deprecated and will not be made available in the New Platform. - // Known usages: - // - x-pack/plugins/infra/public/utils/use_kibana_space_id.ts - const spacesPlugin = server.newPlatform.setup.plugins.spaces as SpacesPluginSetup; - if (!spacesPlugin) { - throw new Error('New Platform XPack Spaces plugin is not available.'); - } - const kibanaRequest = KibanaRequest.from(request); - const spaceId = spacesPlugin.spacesService.getSpaceId(kibanaRequest); - const spacesClient = await spacesPlugin.spacesService.scopedClient(kibanaRequest); - try { - vars.activeSpace = { - valid: true, - space: await spacesClient.get(spaceId), - }; - } catch (e) { - vars.activeSpace = { - valid: false, - error: wrapError(e).output.payload, - }; - } - - return vars; - }, - }, - - async init(server: Server) { - const kbnServer = (server as unknown) as KbnServer; - - const spacesPlugin = kbnServer.newPlatform.setup.plugins.spaces as SpacesPluginSetup; - if (!spacesPlugin) { - throw new Error('New Platform XPack Spaces plugin is not available.'); - } - - server.expose('getSpaceId', (request: Legacy.Request) => - spacesPlugin.spacesService.getSpaceId(request) - ); - server.expose('getActiveSpace', (request: Legacy.Request) => - spacesPlugin.spacesService.getActiveSpace(request) - ); - server.expose('spaceIdToNamespace', spacesPlugin.spacesService.spaceIdToNamespace); - server.expose('namespaceToSpaceId', spacesPlugin.spacesService.namespaceToSpaceId); - server.expose('getBasePath', spacesPlugin.spacesService.getBasePath); - }, - }); diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index 679c75a0d5942..a3bd66e744fda 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -13,7 +13,7 @@ export const xpackMain = (kibana) => { id: 'xpack_main', configPrefix: 'xpack.xpack_main', publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch'], + require: [], config(Joi) { return Joi.object({ diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts index 4ec5bc13eea81..3537d1bf42079 100644 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts +++ b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts @@ -5,9 +5,9 @@ */ import { Legacy } from 'kibana'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from '../../../../../../src/core/server'; -export type CallWithRequest = (...args: any[]) => CallCluster; +export type CallWithRequest = (...args: any[]) => LegacyAPICaller; export declare function callWithRequestFactory( server: Legacy.Server, diff --git a/x-pack/package.json b/x-pack/package.json index de0bf0d922b42..0560b1bebe42b 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -107,7 +107,7 @@ "@types/papaparse": "^5.0.3", "@types/pngjs": "^3.3.2", "@types/pretty-ms": "^5.0.0", - "@types/prop-types": "^15.5.3", + "@types/prop-types": "^15.7.3", "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", "@types/react": "^16.9.36", @@ -160,6 +160,7 @@ "cronstrue": "^1.51.0", "cypress": "5.0.0", "cypress-multi-reporters": "^1.2.3", + "cypress-promise": "^1.1.0", "d3": "3.5.17", "d3-scale": "1.0.7", "dragselect": "1.13.1", @@ -274,7 +275,7 @@ "@babel/runtime": "^7.11.2", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", - "@elastic/eui": "28.4.0", + "@elastic/eui": "29.0.0", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", @@ -353,7 +354,7 @@ "papaparse": "^5.2.0", "pdfmake": "^0.1.65", "pngjs": "3.4.0", - "prop-types": "^15.6.0", + "prop-types": "^15.7.2", "proper-lockfile": "^3.2.0", "puid": "1.0.7", "puppeteer-core": "^1.19.0", diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 573fb0e1be580..adef12454f2d5 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -893,7 +893,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -946,7 +946,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -972,17 +972,21 @@ describe('update()', () => { name: 'my name', config: {}, }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", - "my-action", Object { "actionTypeId": "my-action-type", "config": Object {}, "name": "my name", "secrets": Object {}, }, + Object { + "id": "my-action", + "overwrite": true, + "references": Array [], + }, ] `); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); @@ -1043,7 +1047,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -1081,11 +1085,10 @@ describe('update()', () => { c: true, }, }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", - "my-action", Object { "actionTypeId": "my-action-type", "config": Object { @@ -1096,6 +1099,11 @@ describe('update()', () => { "name": "my name", "secrets": Object {}, }, + Object { + "id": "my-action", + "overwrite": true, + "references": Array [], + }, ] `); }); @@ -1118,7 +1126,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 06c9555f3a18d..4079a6ddeeb8a 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -13,6 +13,7 @@ import { } from 'src/core/server'; import { i18n } from '@kbn/i18n'; +import { omitBy, isUndefined } from 'lodash'; import { ActionTypeRegistry } from './action_type_registry'; import { validateConfig, validateSecrets, ActionExecutorContract } from './lib'; import { @@ -30,7 +31,10 @@ import { } from './create_execute_function'; import { ActionsAuthorization } from './authorization/actions_authorization'; import { ActionType } from '../common'; -import { shouldLegacyRbacApplyBySource } from './authorization/should_legacy_rbac_apply_by_source'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -151,8 +155,10 @@ export class ActionsClient { 'update' ); } - const existingObject = await this.unsecuredSavedObjectsClient.get('action', id); - const { actionTypeId } = existingObject.attributes; + const { attributes, references, version } = await this.unsecuredSavedObjectsClient.get< + RawAction + >('action', id); + const { actionTypeId } = attributes; const { name, config, secrets } = action; const actionType = this.actionTypeRegistry.get(actionTypeId); const validatedActionTypeConfig = validateConfig(actionType, config); @@ -160,12 +166,25 @@ export class ActionsClient { this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - const result = await this.unsecuredSavedObjectsClient.update('action', id, { - actionTypeId, - name, - config: validatedActionTypeConfig as SavedObjectAttributes, - secrets: validatedActionTypeSecrets as SavedObjectAttributes, - }); + const result = await this.unsecuredSavedObjectsClient.create( + 'action', + { + ...attributes, + actionTypeId, + name, + config: validatedActionTypeConfig as SavedObjectAttributes, + secrets: validatedActionTypeSecrets as SavedObjectAttributes, + }, + omitBy( + { + id, + overwrite: true, + references, + version, + }, + isUndefined + ) + ); return { id, @@ -301,7 +320,10 @@ export class ActionsClient { params, source, }: Omit): Promise> { - if (!(await shouldLegacyRbacApplyBySource(this.unsecuredSavedObjectsClient, source))) { + if ( + (await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) === + AuthorizationMode.RBAC + ) { await this.authorization.ensureAuthorized('execute'); } return this.actionExecutor.execute({ actionId, params, source, request: this.request }); @@ -309,7 +331,10 @@ export class ActionsClient { public async enqueueExecution(options: EnqueueExecutionOptions): Promise { const { source } = options; - if (!(await shouldLegacyRbacApplyBySource(this.unsecuredSavedObjectsClient, source))) { + if ( + (await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) === + AuthorizationMode.RBAC + ) { await this.authorization.ensureAuthorized('execute'); } return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options); diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index 08c4472f8007b..a19a662f8323c 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -10,6 +10,7 @@ import { actionsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { ActionsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; import { AuthenticatedUser } from '../../../security/server'; +import { AuthorizationMode } from './get_authorization_mode_by_source'; const request = {} as KibanaRequest; @@ -195,7 +196,7 @@ describe('ensureAuthorized', () => { `); }); - test('exempts users from requiring privileges to execute actions when shouldUseLegacyRbac is true', async () => { + test('exempts users from requiring privileges to execute actions when authorizationMode is Legacy', async () => { const { authorization, authentication } = mockSecurity(); const checkPrivileges: jest.MockedFunction { authorization, authentication, auditLogger, - shouldUseLegacyRbac: true, + authorizationMode: AuthorizationMode.Legacy, }); authentication.getCurrentUser.mockReturnValueOnce(({ diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index bd6e355c2cf9d..cad58bed50981 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -9,6 +9,7 @@ import { KibanaRequest } from 'src/core/server'; import { SecurityPluginSetup } from '../../../security/server'; import { ActionsAuthorizationAuditLogger } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { AuthorizationMode } from './get_authorization_mode_by_source'; export interface ConstructorOptions { request: KibanaRequest; @@ -22,7 +23,7 @@ export interface ConstructorOptions { // actions to continue to execute - which requires that we exempt auth on // `get` for Connectors and `execute` for Action execution when used by // these legacy alerts - shouldUseLegacyRbac?: boolean; + authorizationMode?: AuthorizationMode; } const operationAlias: Record< @@ -43,20 +44,19 @@ export class ActionsAuthorization { private readonly authorization?: SecurityPluginSetup['authz']; private readonly authentication?: SecurityPluginSetup['authc']; private readonly auditLogger: ActionsAuthorizationAuditLogger; - private readonly shouldUseLegacyRbac: boolean; - + private readonly authorizationMode: AuthorizationMode; constructor({ request, authorization, authentication, auditLogger, - shouldUseLegacyRbac = false, + authorizationMode = AuthorizationMode.RBAC, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.authentication = authentication; this.auditLogger = auditLogger; - this.shouldUseLegacyRbac = shouldUseLegacyRbac; + this.authorizationMode = authorizationMode; } public async ensureAuthorized(operation: string, actionTypeId?: string) { @@ -87,6 +87,9 @@ export class ActionsAuthorization { } private isOperationExemptDueToLegacyRbac(operation: string) { - return this.shouldUseLegacyRbac && LEGACY_RBAC_EXEMPT_OPERATIONS.has(operation); + return ( + this.authorizationMode === AuthorizationMode.Legacy && + LEGACY_RBAC_EXEMPT_OPERATIONS.has(operation) + ); } } diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.test.ts similarity index 67% rename from x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts rename to x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.test.ts index 03062994adeb6..4980c476e60ea 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts +++ b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.test.ts @@ -3,88 +3,93 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { shouldLegacyRbacApplyBySource } from './should_legacy_rbac_apply_by_source'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './get_authorization_mode_by_source'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import uuid from 'uuid'; import { asSavedObjectExecutionSource } from '../lib'; const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); -describe(`#shouldLegacyRbacApplyBySource`, () => { - test('should return false if no source is provided', async () => { - expect(await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient)).toEqual(false); +describe(`#getAuthorizationModeBySource`, () => { + test('should return RBAC if no source is provided', async () => { + expect(await getAuthorizationModeBySource(unsecuredSavedObjectsClient)).toEqual( + AuthorizationMode.RBAC + ); }); - test('should return false if source is not an alert', async () => { + test('should return RBAC if source is not an alert', async () => { expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'action', id: uuid.v4(), }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); - test('should return false if source alert is not marked as legacy', async () => { + test('should return RBAC if source alert is not marked as legacy', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue(mockAlert({ id })); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); - test('should return true if source alert is marked as legacy', async () => { + test('should return Legacy if source alert is marked as legacy', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue( mockAlert({ id, attributes: { meta: { versionApiKeyLastmodified: 'pre-7.10.0' } } }) ); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(true); + ).toEqual(AuthorizationMode.Legacy); }); - test('should return false if source alert is marked as modern', async () => { + test('should return RBAC if source alert is marked as modern', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue( mockAlert({ id, attributes: { meta: { versionApiKeyLastmodified: '7.10.0' } } }) ); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); - test('should return false if source alert is marked with a last modified version', async () => { + test('should return RBAC if source alert doesnt have a last modified version', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue(mockAlert({ id, attributes: { meta: {} } })); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); }); diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts similarity index 55% rename from x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts rename to x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts index 06d5776003ede..85d646c75defa 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts +++ b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts @@ -10,18 +10,24 @@ import { ALERT_SAVED_OBJECT_TYPE } from '../saved_objects'; const LEGACY_VERSION = 'pre-7.10.0'; -export async function shouldLegacyRbacApplyBySource( +export enum AuthorizationMode { + Legacy, + RBAC, +} + +export async function getAuthorizationModeBySource( unsecuredSavedObjectsClient: SavedObjectsClientContract, executionSource?: ActionExecutionSource -): Promise { +): Promise { return isSavedObjectExecutionSource(executionSource) && - executionSource?.source?.type === ALERT_SAVED_OBJECT_TYPE - ? ( - await unsecuredSavedObjectsClient.get<{ - meta?: { - versionApiKeyLastmodified?: string; - }; - }>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id) - ).attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION - : false; + executionSource?.source?.type === ALERT_SAVED_OBJECT_TYPE && + ( + await unsecuredSavedObjectsClient.get<{ + meta?: { + versionApiKeyLastmodified?: string; + }; + }>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id) + ).attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION + ? AuthorizationMode.Legacy + : AuthorizationMode.RBAC; } diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts index 321509a7b9de6..abe5921fda7f1 100644 --- a/x-pack/plugins/actions/server/feature.ts +++ b/x-pack/plugins/actions/server/feature.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; export const ACTIONS_FEATURE = { id: 'actions', @@ -14,6 +15,7 @@ export const ACTIONS_FEATURE = { }), icon: 'bell', navLinkId: 'actions', + category: DEFAULT_APP_CATEGORIES.management, app: [], management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index a607dc0de0bda..73434d5c1eaa2 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -27,7 +27,7 @@ export interface ActionExecutorContext { getServices: GetServicesFunction; getActionsClientWithRequest: ( request: KibanaRequest, - executionSource?: ActionExecutionSource + authorizationContext?: ActionExecutionSource ) => Promise>; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actionTypeRegistry: ActionTypeRegistryContract; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 97cefafad4385..dca1114f0ae44 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -71,7 +71,10 @@ import { ACTIONS_FEATURE } from './feature'; import { ActionsAuthorization } from './authorization/actions_authorization'; import { ActionsAuthorizationAuditLogger } from './authorization/audit_logger'; import { ActionExecutionSource } from './lib/action_execution_source'; -import { shouldLegacyRbacApplyBySource } from './authorization/should_legacy_rbac_apply_by_source'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { @@ -281,7 +284,7 @@ export class ActionsPlugin implements Plugin, Plugi const getActionsClientWithRequest = async ( request: KibanaRequest, - source?: ActionExecutionSource + authorizationContext?: ActionExecutionSource ) => { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( @@ -303,7 +306,7 @@ export class ActionsPlugin implements Plugin, Plugi request, authorization: instantiateAuthorization( request, - await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient, source) + await getAuthorizationModeBySource(unsecuredSavedObjectsClient, authorizationContext) ), actionExecutor: actionExecutor!, executionEnqueuer: createExecutionEnqueuerFunction({ @@ -316,7 +319,8 @@ export class ActionsPlugin implements Plugin, Plugi }; // Ensure the public API cannot be used to circumvent authorization - // using our legacy exemption mechanism + // using our legacy exemption mechanism by passing in a legacy SO + // as authorizationContext which would then set a Legacy AuthorizationMode const secureGetActionsClientWithRequest = (request: KibanaRequest) => getActionsClientWithRequest(request); @@ -389,11 +393,11 @@ export class ActionsPlugin implements Plugin, Plugi private instantiateAuthorization = ( request: KibanaRequest, - shouldUseLegacyRbac: boolean = false + authorizationMode?: AuthorizationMode ) => { return new ActionsAuthorization({ request, - shouldUseLegacyRbac, + authorizationMode, authorization: this.security?.authz, authentication: this.security?.authc, auditLogger: new ActionsAuthorizationAuditLogger( diff --git a/x-pack/plugins/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts index 316bae98bf8c1..a7c8b940fbf06 100644 --- a/x-pack/plugins/alerting_builtins/server/feature.ts +++ b/x-pack/plugins/alerting_builtins/server/feature.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; import { BUILT_IN_ALERTS_FEATURE_ID } from '../common'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; export const BUILT_IN_ALERTS_FEATURE = { id: BUILT_IN_ALERTS_FEATURE_ID, @@ -15,6 +16,7 @@ export const BUILT_IN_ALERTS_FEATURE = { }), icon: 'bell', app: [], + category: DEFAULT_APP_CATEGORIES.management, management: { insightsAndAlerting: ['triggersActions'], }, diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 4b5af942024c0..a6cffb0284815 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -220,7 +220,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -270,27 +270,33 @@ describe('create()', () => { test('creates an alert', async () => { const data = getMockData(); + const createdAttributes = { + ...data, + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: '2019-02-12T21:01:22.479Z', + createdBy: 'elastic', + updatedBy: 'elastic', + muteAll: false, + mutedInstanceIds: [], + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }; unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: '2019-02-12T21:01:22.479Z', - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, + attributes: createdAttributes, references: [ { name: 'action_0', @@ -312,11 +318,11 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { - actions: [], + ...createdAttributes, scheduledTaskId: 'task-123', }, references: [ @@ -342,8 +348,14 @@ describe('create()', () => { }, ], "alertTypeId": "123", + "consumer": "bar", "createdAt": 2019-02-12T21:01:22.479Z, + "createdBy": "elastic", + "enabled": true, "id": "1", + "muteAll": false, + "mutedInstanceIds": Array [], + "name": "abc", "params": Object { "bar": true, }, @@ -351,7 +363,12 @@ describe('create()', () => { "interval": "10s", }, "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": null, "updatedAt": 2019-02-12T21:01:22.479Z, + "updatedBy": "elastic", } `); expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -531,7 +548,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -965,7 +982,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1081,7 +1098,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1175,6 +1192,16 @@ describe('enable()', () => { alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); taskManager.schedule.mockResolvedValue({ id: 'task-123', scheduledAt: new Date(), @@ -1233,6 +1260,17 @@ describe('enable()', () => { }); test('enables an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); + await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { @@ -1317,7 +1355,7 @@ describe('enable()', () => { await alertsClient.enable({ id: '1' }); expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); @@ -1384,6 +1422,7 @@ describe('enable()', () => { }); test('throws error when failing to update the first time', async () => { + unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1396,6 +1435,7 @@ describe('enable()', () => { }); test('throws error when failing to update the second time', async () => { + unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ ...existingAlert, attributes: { @@ -1460,6 +1500,8 @@ describe('disable()', () => { ...existingAlert.attributes, apiKey: Buffer.from('123:abc').toString('base64'), }, + version: '123', + references: [], }; beforeEach(() => { @@ -1501,13 +1543,13 @@ describe('disable()', () => { consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, enabled: false, meta: { versionApiKeyLastmodified: kibanaVersion, }, scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, updatedBy: 'elastic', actions: [ { @@ -1544,13 +1586,13 @@ describe('disable()', () => { consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, enabled: false, meta: { versionApiKeyLastmodified: kibanaVersion, }, scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, updatedBy: 'elastic', actions: [ { @@ -1739,6 +1781,7 @@ describe('unmuteAll()', () => { muteAll: true, }, references: [], + version: '123', }); await alertsClient.unmuteAll({ id: '1' }); @@ -1829,7 +1872,9 @@ describe('muteInstance()', () => { mutedInstanceIds: ['2'], updatedBy: 'elastic', }, - { version: '123' } + { + version: '123', + } ); }); @@ -1850,7 +1895,7 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('skips muting when alert is muted', async () => { @@ -1871,7 +1916,7 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); describe('authorization', () => { @@ -1983,7 +2028,7 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('skips unmuting when alert is muted', async () => { @@ -2004,7 +2049,7 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); describe('authorization', () => { @@ -3052,7 +3097,7 @@ describe('update()', () => { }); test('updates given parameters', async () => { - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3189,11 +3234,10 @@ describe('update()', () => { namespace: 'default', }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -3244,8 +3288,10 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` Object { + "id": "1", + "overwrite": true, "references": Array [ Object { "id": "1", @@ -3286,7 +3332,7 @@ describe('update()', () => { apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3365,11 +3411,10 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -3404,18 +3449,20 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); }); it(`doesn't call the createAPIKey function when alert is disabled`, async () => { @@ -3439,7 +3486,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3519,11 +3566,10 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -3558,18 +3604,20 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); }); it('should validate params', async () => { @@ -3627,7 +3675,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3686,7 +3734,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3765,7 +3813,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3919,7 +3967,7 @@ describe('update()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: alertId, type: 'alert', attributes: { @@ -4091,7 +4139,7 @@ describe('update()', () => { describe('authorization', () => { beforeEach(() => { - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 0a08ca848c73d..671b1d6411d7f 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -251,7 +251,7 @@ export class AlertsClient { } throw e; } - await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { + await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { scheduledTaskId: scheduledTask.id, }); createdAlert.attributes.scheduledTaskId = scheduledTask.id; @@ -488,9 +488,8 @@ export class AlertsClient { : null; const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username); - const updatedObject = await this.unsecuredSavedObjectsClient.update( + const updatedObject = await this.unsecuredSavedObjectsClient.create( 'alert', - id, this.updateMeta({ ...attributes, ...data, @@ -500,6 +499,8 @@ export class AlertsClient { updatedBy: username, }), { + id, + overwrite: true, version, references, } @@ -798,6 +799,7 @@ export class AlertsClient { 'alert', alertId ); + await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, @@ -809,7 +811,7 @@ export class AlertsClient { const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { - await this.unsecuredSavedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', alertId, this.updateMeta({ diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 9515987af8dd9..b3c7ada26c456 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -44,6 +44,7 @@ function mockFeature(appName: string, typeName?: string) { id: appName, name: appName, app: [], + category: { id: 'foo', label: 'foo' }, ...(typeName ? { alerting: [typeName], @@ -87,6 +88,7 @@ function mockFeatureWithSubFeature(appName: string, typeName: string) { id: appName, name: appName, app: [], + category: { id: 'foo', label: 'foo' }, ...(typeName ? { alerting: [typeName], diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 026aa0c5238dc..b13a1c62f6602 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -164,6 +164,7 @@ function mockFeatures() { id: 'appName', name: 'appName', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index f873b0178ece9..aca447b6adedd 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -19,6 +19,7 @@ import { AlertInstanceContext, AlertType, AlertTypeParams, + RawAlert, } from '../types'; interface CreateExecutionHandlerOptions { @@ -28,7 +29,7 @@ interface CreateExecutionHandlerOptions { actionsPlugin: ActionsPluginStartContract; actions: AlertAction[]; spaceId: string; - apiKey: string | null; + apiKey: RawAlert['apiKey']; alertType: AlertType; logger: Logger; eventLogger: IEventLogger; @@ -99,7 +100,7 @@ export function createExecutionHandler({ id: action.id, params: action.params, spaceId, - apiKey, + apiKey: apiKey ?? null, source: asSavedObjectExecutionSource({ id: alertId, type: 'alert', diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 4c16d23b485b5..5be684eca4651 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -73,7 +73,7 @@ export class TaskRunner { return apiKey; } - private getFakeKibanaRequest(spaceId: string, apiKey: string | null) { + private getFakeKibanaRequest(spaceId: string, apiKey: RawAlert['apiKey']) { const requestHeaders: Record = {}; if (apiKey) { @@ -98,7 +98,7 @@ export class TaskRunner { private getServicesWithSpaceLevelPermissions( spaceId: string, - apiKey: string | null + apiKey: RawAlert['apiKey'] ): [Services, PublicMethodsOf] { const request = this.getFakeKibanaRequest(spaceId, apiKey); return [this.context.getServices(request), this.context.getAlertsClientWithRequest(request)]; @@ -109,7 +109,7 @@ export class TaskRunner { alertName: string, tags: string[] | undefined, spaceId: string, - apiKey: string | null, + apiKey: RawAlert['apiKey'], actions: Alert['actions'], alertParams: RawAlert['params'] ) { @@ -250,7 +250,11 @@ export class TaskRunner { }; } - async validateAndExecuteAlert(services: Services, apiKey: string | null, alert: SanitizedAlert) { + async validateAndExecuteAlert( + services: Services, + apiKey: RawAlert['apiKey'], + alert: SanitizedAlert + ) { const { params: { alertId, spaceId }, } = this.taskInstance; diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index 15a3c642faf32..a234226d18034 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -5,6 +5,8 @@ */ import { i18n } from '@kbn/i18n'; +import { ValuesType } from 'utility-types'; +import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../../ml/common'; export enum AlertType { ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat. @@ -55,6 +57,41 @@ export const ALERT_TYPES_CONFIG = { }, }; +export const ANOMALY_ALERT_SEVERITY_TYPES = [ + { + type: ANOMALY_SEVERITY.CRITICAL, + label: i18n.translate('xpack.apm.alerts.anomalySeverity.criticalLabel', { + defaultMessage: 'critical', + }), + threshold: ANOMALY_THRESHOLD.CRITICAL, + }, + { + type: ANOMALY_SEVERITY.MAJOR, + label: i18n.translate('xpack.apm.alerts.anomalySeverity.majorLabel', { + defaultMessage: 'major', + }), + threshold: ANOMALY_THRESHOLD.MAJOR, + }, + { + type: ANOMALY_SEVERITY.MINOR, + label: i18n.translate('xpack.apm.alerts.anomalySeverity.minor', { + defaultMessage: 'minor', + }), + threshold: ANOMALY_THRESHOLD.MINOR, + }, + { + type: ANOMALY_SEVERITY.WARNING, + label: i18n.translate('xpack.apm.alerts.anomalySeverity.warningLabel', { + defaultMessage: 'warning', + }), + threshold: ANOMALY_THRESHOLD.WARNING, + }, +] as const; + +export type AnomalyAlertSeverityType = ValuesType< + typeof ANOMALY_ALERT_SEVERITY_TYPES +>['type']; + // Server side registrations // x-pack/plugins/apm/server/lib/alerts/.ts // x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts diff --git a/x-pack/plugins/apm/common/anomaly_detection.test.ts b/x-pack/plugins/apm/common/anomaly_detection.test.ts deleted file mode 100644 index 21963b5300f83..0000000000000 --- a/x-pack/plugins/apm/common/anomaly_detection.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getSeverity, Severity } from './anomaly_detection'; - -describe('getSeverity', () => { - describe('when score is undefined', () => { - it('returns undefined', () => { - expect(getSeverity(undefined)).toEqual(undefined); - }); - }); - - describe('when score < 25', () => { - it('returns warning', () => { - expect(getSeverity(10)).toEqual(Severity.warning); - }); - }); - - describe('when score is between 25 and 50', () => { - it('returns minor', () => { - expect(getSeverity(40)).toEqual(Severity.minor); - }); - }); - - describe('when score is between 50 and 75', () => { - it('returns major', () => { - expect(getSeverity(60)).toEqual(Severity.major); - }); - }); - - describe('when score is 75 or more', () => { - it('returns critical', () => { - expect(getSeverity(100)).toEqual(Severity.critical); - }); - }); -}); diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 5d80ee6381267..dc5731e88083c 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -5,89 +5,31 @@ */ import { i18n } from '@kbn/i18n'; -import { EuiTheme } from '../../../legacy/common/eui_styled_components'; +import { ANOMALY_SEVERITY } from '../../ml/common'; +import { + getSeverityType, + getSeverityColor as mlGetSeverityColor, +} from '../../ml/common'; +import { ServiceHealthStatus } from './service_health_status'; export interface ServiceAnomalyStats { transactionType?: string; anomalyScore?: number; actualValue?: number; jobId?: string; + healthStatus: ServiceHealthStatus; } -export enum Severity { - critical = 'critical', - major = 'major', - minor = 'minor', - warning = 'warning', -} - -// TODO: Replace with `getSeverity` from: -// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129 -export function getSeverity(score?: number) { - if (typeof score !== 'number') { - return undefined; - } else if (score < 25) { - return Severity.warning; - } else if (score >= 25 && score < 50) { - return Severity.minor; - } else if (score >= 50 && score < 75) { - return Severity.major; - } else if (score >= 75) { - return Severity.critical; - } else { - return undefined; +export function getSeverity(score: number | undefined) { + if (score === undefined) { + return ANOMALY_SEVERITY.UNKNOWN; } -} -export function getSeverityColor(theme: EuiTheme, severity?: Severity) { - switch (severity) { - case Severity.warning: - return theme.eui.euiColorVis0; - case Severity.minor: - case Severity.major: - return theme.eui.euiColorVis5; - case Severity.critical: - return theme.eui.euiColorVis9; - default: - return; - } + return getSeverityType(score); } -export function getSeverityLabel(severity?: Severity) { - switch (severity) { - case Severity.critical: - return i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.critical', - { - defaultMessage: 'Critical', - } - ); - - case Severity.major: - case Severity.minor: - return i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.warning', - { - defaultMessage: 'Warning', - } - ); - - case Severity.warning: - return i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.healthy', - { - defaultMessage: 'Healthy', - } - ); - - default: - return i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.unknown', - { - defaultMessage: 'Unknown', - } - ); - } +export function getSeverityColor(score: number) { + return mlGetSeverityColor(score); } export const ML_ERRORS = { diff --git a/x-pack/plugins/apm/common/custom_link/index.ts b/x-pack/plugins/apm/common/custom_link/index.ts new file mode 100644 index 0000000000000..bc0ffefd79c4d --- /dev/null +++ b/x-pack/plugins/apm/common/custom_link/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const INVALID_LICENSE = i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.license.text', + { + defaultMessage: + "To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services.", + } +); diff --git a/x-pack/plugins/apm/common/service_health_status.ts b/x-pack/plugins/apm/common/service_health_status.ts new file mode 100644 index 0000000000000..468f06ab97af8 --- /dev/null +++ b/x-pack/plugins/apm/common/service_health_status.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ANOMALY_SEVERITY } from '../../ml/common'; + +import { EuiTheme } from '../../../legacy/common/eui_styled_components'; + +export enum ServiceHealthStatus { + healthy = 'healthy', + critical = 'critical', + warning = 'warning', + unknown = 'unknown', +} + +export function getServiceHealthStatus({ + severity, +}: { + severity: ANOMALY_SEVERITY; +}) { + switch (severity) { + case ANOMALY_SEVERITY.CRITICAL: + case ANOMALY_SEVERITY.MAJOR: + return ServiceHealthStatus.critical; + + case ANOMALY_SEVERITY.MINOR: + case ANOMALY_SEVERITY.WARNING: + return ServiceHealthStatus.warning; + + case ANOMALY_SEVERITY.LOW: + return ServiceHealthStatus.healthy; + + case ANOMALY_SEVERITY.UNKNOWN: + return ServiceHealthStatus.unknown; + } +} + +export function getServiceHealthStatusColor( + theme: EuiTheme, + status: ServiceHealthStatus +) { + switch (status) { + case ServiceHealthStatus.healthy: + return theme.eui.euiColorVis0; + case ServiceHealthStatus.warning: + return theme.eui.euiColorVis5; + case ServiceHealthStatus.critical: + return theme.eui.euiColorVis9; + case ServiceHealthStatus.unknown: + return theme.eui.euiColorMediumShade; + } +} + +export function getServiceHealthStatusLabel(status: ServiceHealthStatus) { + switch (status) { + case ServiceHealthStatus.critical: + return i18n.translate('xpack.apm.serviceHealthStatus.critical', { + defaultMessage: 'Critical', + }); + + case ServiceHealthStatus.warning: + return i18n.translate('xpack.apm.serviceHealthStatus.warning', { + defaultMessage: 'Warning', + }); + + case ServiceHealthStatus.healthy: + return i18n.translate('xpack.apm.serviceHealthStatus.healthy', { + defaultMessage: 'Healthy', + }); + + case ServiceHealthStatus.unknown: + return i18n.translate('xpack.apm.serviceHealthStatus.unknown', { + defaultMessage: 'Unknown', + }); + } +} diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 8aa4417580337..bdef0f9786a3f 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -23,13 +23,19 @@ ], "server": true, "ui": true, - "configPath": ["xpack", "apm"], - "extraPublicDirs": ["public/style/variables"], + "configPath": [ + "xpack", + "apm" + ], + "extraPublicDirs": [ + "public/style/variables" + ], "requiredBundles": [ "kibanaReact", "kibanaUtils", "observability", "home", - "maps" + "maps", + "ml" ] } diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx index 5bddfc67200b1..468d08339431c 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx @@ -5,105 +5,60 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui'; +import { getSeverityColor } from '../../../../common/anomaly_detection'; import { - getSeverityColor, - Severity, -} from '../../../../common/anomaly_detection'; -import { useTheme } from '../../../hooks/useTheme'; + AnomalyAlertSeverityType, + ANOMALY_ALERT_SEVERITY_TYPES, +} from '../../../../common/alert_types'; -type SeverityScore = 0 | 25 | 50 | 75; -const ANOMALY_SCORES: SeverityScore[] = [0, 25, 50, 75]; - -const anomalyScoreSeverityMap: { - [key in SeverityScore]: { label: string; severity: Severity }; -} = { - 0: { - label: i18n.translate('xpack.apm.alerts.anomalySeverity.warningLabel', { - defaultMessage: 'warning', - }), - severity: Severity.warning, - }, - 25: { - label: i18n.translate('xpack.apm.alerts.anomalySeverity.minorLabel', { - defaultMessage: 'minor', - }), - severity: Severity.minor, - }, - 50: { - label: i18n.translate('xpack.apm.alerts.anomalySeverity.majorLabel', { - defaultMessage: 'major', - }), - severity: Severity.major, - }, - 75: { - label: i18n.translate('xpack.apm.alerts.anomalySeverity.criticalLabel', { - defaultMessage: 'critical', - }), - severity: Severity.critical, - }, -}; - -export function AnomalySeverity({ - severityScore, -}: { - severityScore: SeverityScore; -}) { - const theme = useTheme(); - const { label, severity } = anomalyScoreSeverityMap[severityScore]; - const defaultColor = theme.eui.euiColorMediumShade; - const color = getSeverityColor(theme, severity) || defaultColor; +export function AnomalySeverity({ type }: { type: AnomalyAlertSeverityType }) { + const selectedOption = ANOMALY_ALERT_SEVERITY_TYPES.find( + (option) => option.type === type + )!; return ( - - {label} + + {selectedOption.label} ); } -const getOption = (value: SeverityScore) => { - return { - value: value.toString(10), - inputDisplay: , - dropdownDisplay: ( - <> - - - -

- -

- - - ), - }; -}; - interface Props { - onChange: (value: SeverityScore) => void; - value: SeverityScore; + onChange: (value: AnomalyAlertSeverityType) => void; + value: AnomalyAlertSeverityType; } export function SelectAnomalySeverity({ onChange, value }: Props) { - const options = ANOMALY_SCORES.map((anomalyScore) => getOption(anomalyScore)); - return ( { - const selectedAnomalyScore = parseInt( - selectedValue, - 10 - ) as SeverityScore; - onChange(selectedAnomalyScore); + options={ANOMALY_ALERT_SEVERITY_TYPES.map((option) => ({ + value: option.type, + inputDisplay: , + dropdownDisplay: ( + <> + + + +

+ +

+
+ + ), + }))} + valueOfSelected={value} + onChange={(selectedValue: AnomalyAlertSeverityType) => { + onChange(selectedValue); }} /> ); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index fb4cda56fce04..ca1f55e9d391a 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -7,6 +7,7 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { ANOMALY_SEVERITY } from '../../../../../ml/common'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { useEnvironments } from '../../../hooks/useEnvironments'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; @@ -34,7 +35,11 @@ interface Params { serviceName: string; transactionType: string; environment: string; - anomalyScore: 0 | 25 | 50 | 75; + anomalySeverityType: + | ANOMALY_SEVERITY.CRITICAL + | ANOMALY_SEVERITY.MAJOR + | ANOMALY_SEVERITY.MINOR + | ANOMALY_SEVERITY.WARNING; } interface Props { @@ -67,7 +72,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { transactionType, serviceName, environment: urlParams.environment || ENVIRONMENT_ALL.value, - anomalyScore: 75, + anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, }; const params = { @@ -84,7 +89,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { onChange={(e) => setAlertParams('environment', e.target.value)} />, } + value={} title={i18n.translate( 'xpack.apm.transactionDurationAnomalyAlertTrigger.anomalySeverity', { @@ -93,9 +98,9 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { )} > { - setAlertParams('anomalyScore', value); + setAlertParams('anomalySeverityType', value); }} /> , diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx index b027609fd3a7f..0135f0b369537 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx @@ -28,7 +28,7 @@ export function CoreVitals({ data, loading }: Props) { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index bb5d37a10fb33..94c3acfaa9727 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -23,7 +23,7 @@ export interface UXMetrics { cls: string; fid: string; lcp: string; - tbt: string; + tbt: number; fcp: number; lcpRanks: number[]; fidRanks: number[]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx index 790be81bb65c0..388a8824bc73d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx @@ -8,7 +8,7 @@ import { render } from 'enzyme'; import React from 'react'; import { EmbeddedMap } from '../EmbeddedMap'; -import { KibanaContextProvider } from '../../../../../../../security_solution/public/common/lib/kibana'; +import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; import { embeddablePluginMock } from '../../../../../../../../../src/plugins/embeddable/public/mocks'; describe('Embedded Map', () => { 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 5699d0b56219b..c1192f5f18274 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 @@ -14,6 +14,10 @@ import { EuiIconTip, EuiHealth, } from '@elastic/eui'; +import { + getServiceHealthStatus, + getServiceHealthStatusColor, +} from '../../../../../common/service_health_status'; import { useTheme } from '../../../../hooks/useTheme'; import { fontSize, px } from '../../../../style/variables'; import { asInteger, asDuration } from '../../../../utils/formatters'; @@ -22,7 +26,6 @@ import { popoverWidth } from '../cytoscapeOptions'; import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; import { getSeverity, - getSeverityColor, ServiceAnomalyStats, } from '../../../../../common/anomaly_detection'; @@ -59,13 +62,15 @@ export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) { const theme = useTheme(); const anomalyScore = serviceAnomalyStats?.anomalyScore; - const anomalySeverity = getSeverity(anomalyScore); + const severity = getSeverity(anomalyScore); const actualValue = serviceAnomalyStats?.actualValue; const mlJobId = serviceAnomalyStats?.jobId; const transactionType = serviceAnomalyStats?.transactionType ?? TRANSACTION_REQUEST; const hasAnomalyDetectionScore = anomalyScore !== undefined; + const healthStatus = getServiceHealthStatus({ severity }); + return ( <>
@@ -81,7 +86,9 @@ export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) { - + {ANOMALY_DETECTION_SCORE_METRIC} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 1ac7157cc2aad..61ac9bd7cd54c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -5,26 +5,26 @@ */ import cytoscape from 'cytoscape'; import { CSSProperties } from 'react'; +import { + getServiceHealthStatusColor, + ServiceHealthStatus, +} from '../../../../common/service_health_status'; import { SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE, } from '../../../../common/elasticsearch_fieldnames'; import { EuiTheme } from '../../../../../observability/public'; import { defaultIcon, iconForNode } from './icons'; -import { - getSeverity, - getSeverityColor, - ServiceAnomalyStats, - Severity, -} from '../../../../common/anomaly_detection'; +import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; export const popoverWidth = 280; -function getNodeSeverity(el: cytoscape.NodeSingular) { +function getServiceAnomalyStats(el: cytoscape.NodeSingular) { const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data( 'serviceAnomalyStats' ); - return getSeverity(serviceAnomalyStats?.anomalyScore); + + return serviceAnomalyStats; } function getBorderColorFn( @@ -32,10 +32,11 @@ function getBorderColorFn( ): cytoscape.Css.MapperFunction { return (el: cytoscape.NodeSingular) => { const hasAnomalyDetectionJob = el.data('serviceAnomalyStats') !== undefined; - const nodeSeverity = getNodeSeverity(el); + const anomalyStats = getServiceAnomalyStats(el); if (hasAnomalyDetectionJob) { - return ( - getSeverityColor(theme, nodeSeverity) || theme.eui.euiColorMediumShade + return getServiceHealthStatusColor( + theme, + anomalyStats?.healthStatus ?? ServiceHealthStatus.unknown ); } if (el.hasClass('primary') || el.selected()) { @@ -49,8 +50,8 @@ const getBorderStyle: cytoscape.Css.MapperFunction< cytoscape.NodeSingular, cytoscape.Css.LineStyle > = (el: cytoscape.NodeSingular) => { - const nodeSeverity = getNodeSeverity(el); - if (nodeSeverity === Severity.critical) { + const status = getServiceAnomalyStats(el)?.healthStatus; + if (status === ServiceHealthStatus.critical) { return 'double'; } else { return 'solid'; @@ -58,11 +59,11 @@ const getBorderStyle: cytoscape.Css.MapperFunction< }; function getBorderWidth(el: cytoscape.NodeSingular) { - const nodeSeverity = getNodeSeverity(el); + const status = getServiceAnomalyStats(el)?.healthStatus; - if (nodeSeverity === Severity.minor || nodeSeverity === Severity.major) { + if (status === ServiceHealthStatus.warning) { return 4; - } else if (nodeSeverity === Severity.critical) { + } else if (status === ServiceHealthStatus.critical) { return 8; } else { return 4; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx index 94353080bc7d5..c6be0a352ef66 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx @@ -6,20 +6,22 @@ import React from 'react'; import { EuiBadge } from '@elastic/eui'; import { - getSeverityColor, - getSeverityLabel, - Severity, -} from '../../../../../common/anomaly_detection'; + getServiceHealthStatusColor, + getServiceHealthStatusLabel, + ServiceHealthStatus, +} from '../../../../../common/service_health_status'; import { useTheme } from '../../../../hooks/useTheme'; -export function HealthBadge({ severity }: { severity?: Severity }) { +export function HealthBadge({ + healthStatus, +}: { + healthStatus: ServiceHealthStatus; +}) { const theme = useTheme(); - const unknownColor = theme.eui.euiColorLightShade; - return ( - - {getSeverityLabel(severity)} + + {getServiceHealthStatusLabel(healthStatus)} ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js index 519d74827097b..7c306c16cca1f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js @@ -9,6 +9,7 @@ import { shallow } from 'enzyme'; import { ServiceList, SERVICE_COLUMNS } from '../index'; import props from './props.json'; import { mockMoment } from '../../../../../utils/testHelpers'; +import { ServiceHealthStatus } from '../../../../../../common/service_health_status'; describe('ServiceOverview -> List', () => { beforeAll(() => { @@ -52,25 +53,28 @@ describe('ServiceOverview -> List', () => { describe('without ML data', () => { it('does not render health column', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); const columns = wrapper.props().columns; - expect(columns[0].field).not.toBe('severity'); + expect(columns[0].field).not.toBe('healthStatus'); }); }); describe('with ML data', () => { it('renders health column', () => { const wrapper = shallow( - + ({ + ...item, + healthStatus: ServiceHealthStatus.warning, + }))} + /> ); const columns = wrapper.props().columns; - expect(columns[0].field).toBe('severity'); + expect(columns[0].field).toBe('healthStatus'); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index da3f6ae89940a..e6a9823f3ee28 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -1,6 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ServiceOverview -> List renders columns correctly 1`] = ``; +exports[`ServiceOverview -> List renders columns correctly 1`] = ` + +`; exports[`ServiceOverview -> List renders empty state 1`] = ` List renders empty state 1`] = ` } initialPageSize={50} initialSortDirection="desc" - initialSortField="severity" + initialSortField="healthStatus" items={Array []} sortFn={[Function]} /> @@ -106,7 +110,7 @@ exports[`ServiceOverview -> List renders with data 1`] = ` } initialPageSize={50} initialSortDirection="desc" - initialSortField="severity" + initialSortField="healthStatus" items={ Array [ Object { diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index ce256137481cb..4c7c0824a7c49 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; +import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { asPercent } from '../../../../../common/utils/formatters'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; @@ -20,14 +21,12 @@ import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; import { AgentIcon } from '../../../shared/AgentIcon'; -import { Severity } from '../../../../../common/anomaly_detection'; import { HealthBadge } from './HealthBadge'; import { ServiceListMetric } from './ServiceListMetric'; interface Props { items: ServiceListAPIResponse['items']; noItemsMessage?: React.ReactNode; - displayHealthStatus: boolean; } type ServiceListItem = ValuesType; @@ -53,14 +52,18 @@ const AppLink = styled(TransactionOverviewLink)` export const SERVICE_COLUMNS: Array> = [ { - field: 'severity', + field: 'healthStatus', name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { defaultMessage: 'Health', }), width: px(unit * 6), sortable: true, - render: (_, { severity }) => { - return ; + render: (_, { healthStatus }) => { + return ( + + ); }, }, { @@ -172,40 +175,38 @@ export const SERVICE_COLUMNS: Array> = [ }, ]; -const SEVERITY_ORDER = [ - Severity.warning, - Severity.minor, - Severity.major, - Severity.critical, +const SERVICE_HEALTH_STATUS_ORDER = [ + ServiceHealthStatus.unknown, + ServiceHealthStatus.healthy, + ServiceHealthStatus.warning, + ServiceHealthStatus.critical, ]; -export function ServiceList({ - items, - displayHealthStatus, - noItemsMessage, -}: Props) { +export function ServiceList({ items, noItemsMessage }: Props) { + const displayHealthStatus = items.some((item) => 'healthStatus' in item); + const columns = displayHealthStatus ? SERVICE_COLUMNS - : SERVICE_COLUMNS.filter((column) => column.field !== 'severity'); + : SERVICE_COLUMNS.filter((column) => column.field !== 'healthStatus'); return ( { - // For severity, sort items by severity first, then by TPM + // For healthStatus, sort items by healthStatus first, then by TPM - return sortField === 'severity' + return sortField === 'healthStatus' ? orderBy( itemsToSort, [ (item) => { - return item.severity - ? SEVERITY_ORDER.indexOf(item.severity) + return item.healthStatus + ? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus) : -1; }, (item) => item.transactionsPerMinute?.value ?? 0, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index e4ba1e36378d9..d8c8f25616560 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -10,6 +10,7 @@ import { merge } from 'lodash'; import React, { FunctionComponent, ReactChild } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; +import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { ServiceOverview } from '..'; import { EuiThemeProvider } from '../../../../../../observability/public'; import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; @@ -114,7 +115,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 200, avgResponseTime: 300, environments: ['test', 'dev'], - severity: 1, + healthStatus: ServiceHealthStatus.warning, }, { serviceName: 'My Go Service', @@ -123,7 +124,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 500, avgResponseTime: 600, environments: [], - severity: 10, + severity: ServiceHealthStatus.healthy, }, ], }); @@ -252,7 +253,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 200, avgResponseTime: 300, environments: ['test', 'dev'], - severity: 1, + healthStatus: ServiceHealthStatus.warning, }, ], }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index b56f7d6820274..dfc0cc8637ff1 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -153,8 +153,8 @@ NodeList [ > - Unknown + Warning @@ -435,7 +435,7 @@ NodeList [ > 'severity' in item); - return ( <> @@ -134,7 +132,6 @@ export function ServiceOverview() { ) ) : ( - + )} diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index e806f556347f1..e64357c085209 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -6,6 +6,7 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import { max } from 'lodash'; import React, { useCallback } from 'react'; import { useParams } from 'react-router-dom'; import { asPercent } from '../../../../../common/utils/formatters'; @@ -56,6 +57,7 @@ export function ErroneousTransactionsRateChart() { ); const errorRates = data?.erroneousTransactionsRate || []; + const maxRate = max(errorRates.map((errorRate) => errorRate.y)); return ( @@ -70,7 +72,7 @@ export function ErroneousTransactionsRateChart() { = { + serviceMaps: { + name: 'APM service maps', + license: 'platinum', + }, + ml: { + name: 'APM machine learning', + license: 'platinum', + }, + customLinks: { + name: 'APM custom links', + license: 'gold', + }, +}; + +export function registerFeaturesUsage({ + licensingPlugin, +}: { + licensingPlugin: LicensingPluginSetup; +}) { + Object.values(features).forEach(({ name, license }) => { + licensingPlugin.featureUsage.register(name, license); + }); +} + +export function notifyFeatureUsage({ + licensingPlugin, + featureName, +}: { + licensingPlugin: LicensingRequestHandlerContext; + featureName: FeatureName; +}) { + const feature = features[featureName]; + licensingPlugin.featureUsage.notifyUsage(feature.name); +} diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index b3526b6a97ad9..61cd79b672735 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -6,8 +6,13 @@ import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; +import { ANOMALY_SEVERITY } from '../../../../ml/common'; import { KibanaRequest } from '../../../../../../src/core/server'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + ALERT_TYPES_CONFIG, + ANOMALY_ALERT_SEVERITY_TYPES, +} from '../../../common/alert_types'; import { AlertingPlugin } from '../../../../alerts/server'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; @@ -26,7 +31,12 @@ const paramsSchema = schema.object({ windowSize: schema.number(), windowUnit: schema.string(), environment: schema.string(), - anomalyScore: schema.number(), + anomalySeverityType: schema.oneOf([ + schema.literal(ANOMALY_SEVERITY.CRITICAL), + schema.literal(ANOMALY_SEVERITY.MAJOR), + schema.literal(ANOMALY_SEVERITY.MINOR), + schema.literal(ANOMALY_SEVERITY.WARNING), + ]), }); const alertTypeConfig = @@ -67,6 +77,18 @@ export function registerTransactionDurationAnomalyAlertType({ alertParams.environment ); + const selectedOption = ANOMALY_ALERT_SEVERITY_TYPES.find( + (option) => option.type === alertParams.anomalySeverityType + ); + + if (!selectedOption) { + throw new Error( + `Anomaly alert severity type ${alertParams.anomalySeverityType} is not supported.` + ); + } + + const threshold = selectedOption.threshold; + if (mlJobIds.length === 0) { return {}; } @@ -96,7 +118,7 @@ export function registerTransactionDurationAnomalyAlertType({ { range: { record_score: { - gte: alertParams.anomalyScore, + gte: threshold, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index 4fcfb53a05887..2ff0173b9ac12 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -127,7 +127,7 @@ export async function getWebCoreVitals({ cls: String(cls?.values['50.0']?.toFixed(2) || 0), fid: ((fid?.values['50.0'] || 0) / 1000).toFixed(2), lcp: ((lcp?.values['50.0'] || 0) / 1000).toFixed(2), - tbt: ((tbt?.values['50.0'] || 0) / 1000).toFixed(2), + tbt: tbt?.values['50.0'] || 0, fcp: fcp?.values['50.0'] || 0, lcpRanks: getRanksPercentages(lcpRanks?.values ?? defaultRanks), diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index ed8ae923e6e6c..da087b4c1911a 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from 'boom'; +import { getServiceHealthStatus } from '../../../common/service_health_status'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseReturnType } from '../../../typings/common'; import { @@ -12,6 +13,7 @@ import { } from '../../../common/transaction_types'; import { ServiceAnomalyStats, + getSeverity, ML_ERRORS, } from '../../../common/anomaly_detection'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; @@ -130,13 +132,19 @@ function transformResponseToServiceAnomalies( response.aggregations?.services.buckets ?? [] ).reduce( (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => { + const anomalyScore = topScoreAgg.hits.hits[0]?.sort?.[0]; + + const severity = getSeverity(anomalyScore); + const healthStatus = getServiceHealthStatus({ severity }); + return { ...statsByServiceName, [serviceName]: { transactionType: topScoreAgg.hits.hits[0]?._source?.by_field_value, - anomalyScore: topScoreAgg.hits.hits[0]?.sort?.[0], + anomalyScore, actualValue: topScoreAgg.hits.hits[0]?._source?.actual?.[0], jobId: topScoreAgg.hits.hits[0]?._source?.job_id, + healthStatus, }, }; }, diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index e529198e717d3..f30b80feda302 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ServiceHealthStatus } from '../../../common/service_health_status'; + import { AGENT_NAME, SERVICE_ENVIRONMENT, @@ -43,6 +45,7 @@ const anomalies = { actualValue: 10000, anomalyScore: 50, jobId: 'apm-test-1234-ml-module-name', + healthStatus: ServiceHealthStatus.warning, }, }, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index e7e18cbff1c15..17799203fe73b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getServiceHealthStatus } from '../../../../common/service_health_status'; import { EventOutcome } from '../../../../common/event_outcome'; import { getSeverity } from '../../../../common/anomaly_detection'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; @@ -413,10 +414,11 @@ export const getHealthStatuses = async ( const stats = anomalies.serviceAnomalies[serviceName]; const severity = getSeverity(stats.anomalyScore); + const healthStatus = getServiceHealthStatus({ severity }); return { serviceName, - severity, + healthStatus, }; }); }; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f25e37927f094..b417f8689b229 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -26,11 +26,7 @@ import { MlPluginSetup } from '../../ml/server'; import { ObservabilityPluginSetup } from '../../observability/server'; import { SecurityPluginSetup } from '../../security/server'; import { TaskManagerSetupContract } from '../../task_manager/server'; -import { - APM_FEATURE, - APM_SERVICE_MAPS_FEATURE_NAME, - APM_SERVICE_MAPS_LICENSE_TYPE, -} from './feature'; +import { APM_FEATURE, registerFeaturesUsage } from './feature'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client'; @@ -128,10 +124,8 @@ export class APMPlugin implements Plugin { }); plugins.features.registerKibanaFeature(APM_FEATURE); - plugins.licensing.featureUsage.register( - APM_SERVICE_MAPS_FEATURE_NAME, - APM_SERVICE_MAPS_LICENSE_TYPE - ); + + registerFeaturesUsage({ licensingPlugin: plugins.licensing }); createApmApi().init(core, { config$: mergedConfig$, diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 04807cfac1cea..1996d4d4a262d 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -15,7 +15,7 @@ import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; import { createRoute } from './create_route'; import { rangeRt, uiFiltersRt } from './default_api_types'; -import { APM_SERVICE_MAPS_FEATURE_NAME } from '../feature'; +import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; @@ -37,7 +37,11 @@ export const serviceMapRoute = createRoute(() => ({ if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } - context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME); + + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'serviceMaps', + }); const logger = context.logger; const setup = await setupRequest(context, request); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 2cc0cdb1c2b91..f0a22356d074b 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -15,6 +15,7 @@ import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; +import { notifyFeatureUsage } from '../../feature'; // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute(() => ({ @@ -62,6 +63,10 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ } await createAnomalyDetectionJobs(setup, environments, context.logger); + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'ml', + }); }, })); diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index 83c23a75e999d..7882383d78ab0 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -3,9 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import Boom from 'boom'; import * as t from 'io-ts'; import { pick } from 'lodash'; +import { INVALID_LICENSE } from '../../../common/custom_link'; +import { ILicense } from '../../../../licensing/common/types'; import { FILTER_OPTIONS } from '../../../common/custom_link/custom_link_filter_options'; +import { notifyFeatureUsage } from '../../feature'; import { setupRequest } from '../../lib/helpers/setup_request'; import { createOrUpdateCustomLink } from '../../lib/settings/custom_link/create_or_update_custom_link'; import { @@ -17,6 +22,10 @@ import { getTransaction } from '../../lib/settings/custom_link/get_transaction'; import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links'; import { createRoute } from '../create_route'; +function isActiveGoldLicense(license: ILicense) { + return license.isActive && license.hasAtLeast('gold'); +} + export const customLinkTransactionRoute = createRoute(() => ({ path: '/api/apm/settings/custom_links/transaction', params: { @@ -37,6 +46,9 @@ export const listCustomLinksRoute = createRoute(() => ({ query: filterOptionsRt, }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { query } = context.params; // picks only the items listed in FILTER_OPTIONS @@ -55,9 +67,17 @@ export const createCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const customLink = context.params.body; const res = await createOrUpdateCustomLink({ customLink, setup }); + + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'customLinks', + }); return res; }, })); @@ -75,6 +95,9 @@ export const updateCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { id } = context.params.path; const customLink = context.params.body; @@ -99,6 +122,9 @@ export const deleteCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { id } = context.params.path; const res = await deleteCustomLink({ diff --git a/x-pack/plugins/canvas/server/collectors/collector.ts b/x-pack/plugins/canvas/server/collectors/collector.ts index 48396d93d13e6..eb650ca5ad152 100644 --- a/x-pack/plugins/canvas/server/collectors/collector.ts +++ b/x-pack/plugins/canvas/server/collectors/collector.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { LegacyAPICaller } from 'kibana/server'; import { TelemetryCollector } from '../../types'; import { workpadCollector } from './workpad_collector'; @@ -32,7 +32,7 @@ export function registerCanvasUsageCollector( const canvasCollector = usageCollection.makeUsageCollector({ type: 'canvas', isReady: () => true, - fetch: async (callCluster: CallCluster) => { + fetch: async (callCluster: LegacyAPICaller) => { const collectorResults = await Promise.all( collectors.map((collector) => collector(kibanaIndex, callCluster)) ); diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 9a41a00883c13..ac5392c9d3dee 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -10,6 +10,7 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { initRoutes } from './routes'; import { registerCanvasUsageCollector } from './collectors'; @@ -40,7 +41,8 @@ export class CanvasPlugin implements Plugin { plugins.features.registerKibanaFeature({ id: 'canvas', name: 'Canvas', - order: 400, + order: 300, + category: DEFAULT_APP_CATEGORIES.kibana, icon: 'canvasApp', navLinkId: 'canvas', app: ['canvas', 'kibana'], diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 3517c958b27b8..466a7cc20a497 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -38,10 +38,6 @@ module.exports = { 'src/plugins/data/public/expressions/interpreter' ), 'kbn/interpreter': path.resolve(KIBANA_ROOT, 'packages/kbn-interpreter/target/common'), - 'types/interpreter': path.resolve( - KIBANA_ROOT, - 'src/legacy/core_plugins/interpreter/public/types' - ), tinymath: path.resolve(KIBANA_ROOT, 'node_modules/tinymath/lib/tinymath.es5.js'), core_app_image_assets: path.resolve(KIBANA_ROOT, 'src/core/public/core_app/images'), }, diff --git a/x-pack/plugins/canvas/types/telemetry.ts b/x-pack/plugins/canvas/types/telemetry.ts index 0b354d7677f6e..3b635b2e57926 100644 --- a/x-pack/plugins/canvas/types/telemetry.ts +++ b/x-pack/plugins/canvas/types/telemetry.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; /** Function for collecting information about canvas usage @@ -13,7 +13,7 @@ export type TelemetryCollector = ( /** The server instance */ kibanaIndex: string, /** Function for calling elasticsearch */ - callCluster: CallCluster + callCluster: LegacyAPICaller ) => Record; export interface TelemetryCustomElementDocument { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts index c21109f8a596a..330a501a78d39 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts @@ -7,10 +7,11 @@ import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public'; import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public'; -export interface Config { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type Config = { dashboardId?: string; useCurrentFilters: boolean; useCurrentDateRange: boolean; -} +}; export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext; diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx index 04f60662d88a3..85e92d0827daa 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx @@ -51,6 +51,7 @@ export class UrlDrilldown implements Drilldown txtUrlDrilldownDisplayName; diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts index 8881b2063c8db..e0960b83b23f9 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts @@ -5,6 +5,7 @@ */ import { + DynamicActionsState, UiActionsEnhancedAbstractActionStorage as AbstractActionStorage, UiActionsEnhancedSerializedEvent as SerializedEvent, } from '../../../ui_actions_enhanced/public'; @@ -13,12 +14,12 @@ import { EmbeddableOutput, IEmbeddable, } from '../../../../../src/plugins/embeddable/public'; +import { SerializableState } from '../../../../../src/plugins/kibana_utils/common'; export interface EmbeddableWithDynamicActionsInput extends EmbeddableInput { enhancements?: { - dynamicActions?: { - events: SerializedEvent[]; - }; + dynamicActions: DynamicActionsState; + [key: string]: SerializableState; }; } diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index 18834f55af0a5..e516ab1cb73b3 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -82,6 +82,58 @@ describe('#create', () => { expect(mockBaseClient.create).not.toHaveBeenCalled(); }); + it('allows a specified ID when overwriting an existing object', async () => { + const attributes = { + attrOne: 'one', + attrSecret: 'secret', + attrNotSoSecret: 'not-so-secret', + attrThree: 'three', + }; + const options = { id: 'predefined-uuid', overwrite: true, version: 'some-version' }; + const mockedResponse = { + id: 'predefined-uuid', + type: 'known-type', + attributes: { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + references: [], + }; + + mockBaseClient.create.mockResolvedValue(mockedResponse); + + expect(await wrapper.create('known-type', attributes, options)).toEqual({ + ...mockedResponse, + attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' }, + }); + + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { type: 'known-type', id: 'predefined-uuid' }, + { + attrOne: 'one', + attrSecret: 'secret', + attrNotSoSecret: 'not-so-secret', + attrThree: 'three', + }, + { user: mockAuthenticatedUser() } + ); + + expect(mockBaseClient.create).toHaveBeenCalledTimes(1); + expect(mockBaseClient.create).toHaveBeenCalledWith( + 'known-type', + { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + { id: 'predefined-uuid', overwrite: true, version: 'some-version' } + ); + }); + it('generates ID, encrypts attributes and strips them from response except for ones with `dangerouslyExposeValue` set to `true`', async () => { const attributes = { attrOne: 'one', @@ -262,6 +314,77 @@ describe('#bulkCreate', () => { expect(mockBaseClient.bulkCreate).not.toHaveBeenCalled(); }); + it('allows a specified ID when overwriting an existing object', async () => { + const attributes = { + attrOne: 'one', + attrSecret: 'secret', + attrNotSoSecret: 'not-so-secret', + attrThree: 'three', + }; + const mockedResponse = { + saved_objects: [ + { + id: 'predefined-uuid', + type: 'known-type', + attributes: { ...attributes, attrSecret: '*secret*', attrNotSoSecret: '*not-so-secret*' }, + references: [], + }, + { + id: 'some-id', + type: 'unknown-type', + attributes, + references: [], + }, + ], + }; + + mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse); + + const bulkCreateParams = [ + { id: 'predefined-uuid', type: 'known-type', attributes, version: 'some-version' }, + { type: 'unknown-type', attributes }, + ]; + + await expect(wrapper.bulkCreate(bulkCreateParams, { overwrite: true })).resolves.toEqual({ + saved_objects: [ + { + ...mockedResponse.saved_objects[0], + attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' }, + }, + mockedResponse.saved_objects[1], + ], + }); + + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith( + { type: 'known-type', id: 'predefined-uuid' }, + { + attrOne: 'one', + attrSecret: 'secret', + attrNotSoSecret: 'not-so-secret', + attrThree: 'three', + }, + { user: mockAuthenticatedUser() } + ); + + expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith( + [ + { + ...bulkCreateParams[0], + attributes: { + attrOne: 'one', + attrSecret: '*secret*', + attrNotSoSecret: '*not-so-secret*', + attrThree: 'three', + }, + }, + bulkCreateParams[1], + ], + { overwrite: true } + ); + }); + it('generates ID, encrypts attributes and strips them from response except for ones with `dangerouslyExposeValue` set to `true`', async () => { const attributes = { attrOne: 'one', diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index 0eeb9943b5be9..eef389186d670 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -68,13 +68,18 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon // Saved objects with encrypted attributes should have IDs that are hard to guess especially // since IDs are part of the AAD used during encryption, that's why we control them within this // wrapper and don't allow consumers to specify their own IDs directly. - if (options.id) { + + // only allow a specified ID if we're overwriting an existing ESO with a Version + // this helps us ensure that the document really was previously created using ESO + // and not being used to get around the specified ID limitation + const canSpecifyID = options.overwrite && options.version; + if (options.id && !canSpecifyID) { throw new Error( 'Predefined IDs are not allowed for saved objects with encrypted attributes.' ); } - const id = generateID(); + const id = options.id ?? generateID(); const namespace = getDescriptorNamespace( this.options.baseTypeRegistry, type, @@ -97,7 +102,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon public async bulkCreate( objects: Array>, - options?: SavedObjectsBaseOptions + options?: SavedObjectsBaseOptions & Pick ) { // We encrypt attributes for every object in parallel and that can potentially exhaust libuv or // NodeJS thread pool. If it turns out to be a problem, we can consider switching to the @@ -110,14 +115,15 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon // Saved objects with encrypted attributes should have IDs that are hard to guess especially // since IDs are part of the AAD used during encryption, that's why we control them within this - // wrapper and don't allow consumers to specify their own IDs directly. - if (object.id) { + // wrapper and don't allow consumers to specify their own IDs directly unless overwriting the original document. + const canSpecifyID = options?.overwrite && object.version; + if (object.id && !canSpecifyID) { throw new Error( 'Predefined IDs are not allowed for saved objects with encrypted attributes.' ); } - const id = generateID(); + const id = object.id ?? generateID(); const namespace = getDescriptorNamespace( this.options.baseTypeRegistry, object.type, diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 3d28a05a4b7b4..a9bd03e8f97d4 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -16,6 +16,7 @@ import { KibanaRequest, } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -82,6 +83,7 @@ export class EnterpriseSearchPlugin implements Plugin { id: ENTERPRISE_SEARCH_PLUGIN.ID, name: ENTERPRISE_SEARCH_PLUGIN.NAME, order: 0, + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, icon: 'logoEnterpriseSearch', app: [ 'kibana', diff --git a/x-pack/plugins/features/common/kibana_feature.ts b/x-pack/plugins/features/common/kibana_feature.ts index a600ada554afd..32a7502956728 100644 --- a/x-pack/plugins/features/common/kibana_feature.ts +++ b/x-pack/plugins/features/common/kibana_feature.ts @@ -5,6 +5,7 @@ */ import { RecursiveReadonly } from '@kbn/utility-types'; +import { AppCategory } from 'src/core/types'; import { FeatureKibanaPrivileges } from './feature_kibana_privileges'; import { SubFeatureConfig, SubFeature as KibanaSubFeature } from './sub_feature'; import { ReservedKibanaPrivilege } from './reserved_kibana_privilege'; @@ -29,6 +30,13 @@ export interface KibanaFeatureConfig { */ name: string; + /** + * The category for this feature. + * This will be used to organize the list of features for display within the + * Spaces and Roles management screens. + */ + category: AppCategory; + /** * An ordinal used to sort features relative to one another for display. */ @@ -158,6 +166,10 @@ export class KibanaFeature { return this.config.order; } + public get category() { + return this.config.category; + } + public get navLinkId() { return this.config.navLinkId; } diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index e89cf06ec8621..aaaeccbd15e72 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -14,6 +14,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }; @@ -35,6 +36,7 @@ describe('FeatureRegistry', () => { icon: 'addDataApp', navLinkId: 'someNavLink', app: ['app1'], + category: { id: 'foo', label: 'foo' }, validLicenses: ['standard', 'basic', 'gold', 'platinum'], catalogue: ['foo'], management: { @@ -143,11 +145,64 @@ describe('FeatureRegistry', () => { expect(result[0].toRaw()).toEqual(feature); }); + describe('category', () => { + it('is required', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + } as any; + + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"category\\" fails because [\\"category\\" is required]"` + ); + }); + + it('must have an id', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + category: { label: 'foo' }, + } as any; + + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"category\\" fails because [child \\"id\\" fails because [\\"id\\" is required]]"` + ); + }); + + it('must have a label', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + category: { id: 'foo' }, + } as any; + + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"category\\" fails because [child \\"label\\" fails because [\\"label\\" is required]]"` + ); + }); + }); + it(`requires a value for privileges`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, } as any; const featureRegistry = new FeatureRegistry(); @@ -163,6 +218,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, subFeatures: [ { @@ -201,6 +257,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { ui: [], @@ -235,6 +292,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { ui: [], @@ -271,6 +329,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'foo', @@ -303,6 +362,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { ui: [], @@ -340,6 +400,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }; @@ -347,6 +408,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Duplicate Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }; @@ -367,6 +429,7 @@ describe('FeatureRegistry', () => { name: 'some feature', navLinkId: prohibitedChars, app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }) ).toThrowErrorMatchingSnapshot(); @@ -382,6 +445,7 @@ describe('FeatureRegistry', () => { kibana: [prohibitedChars], }, app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }) ).toThrowErrorMatchingSnapshot(); @@ -395,6 +459,7 @@ describe('FeatureRegistry', () => { name: 'some feature', catalogue: [prohibitedChars], app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }) ).toThrowErrorMatchingSnapshot(); @@ -409,6 +474,7 @@ describe('FeatureRegistry', () => { id: prohibitedId, name: 'some feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }) ).toThrowErrorMatchingSnapshot(); @@ -420,6 +486,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: ['app1', 'app2'], + category: { id: 'foo', label: 'foo' }, privileges: { foo: { name: 'Foo', @@ -447,6 +514,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: ['bar'], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -481,6 +549,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: ['foo', 'bar', 'baz'], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -538,6 +607,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: ['bar'], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'something', @@ -571,6 +641,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: ['foo', 'bar', 'baz'], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'something', @@ -604,6 +675,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['bar'], privileges: { all: { @@ -641,6 +713,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['foo', 'bar', 'baz'], privileges: { all: { @@ -701,6 +774,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['bar'], privileges: null, reserved: { @@ -736,6 +810,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['foo', 'bar', 'baz'], privileges: null, reserved: { @@ -771,6 +846,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, alerting: ['bar'], privileges: { all: { @@ -811,6 +887,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, alerting: ['foo', 'bar', 'baz'], privileges: { all: { @@ -871,6 +948,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, alerting: ['bar'], privileges: null, reserved: { @@ -906,6 +984,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, alerting: ['foo', 'bar', 'baz'], privileges: null, reserved: { @@ -941,6 +1020,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['bar'], management: { kibana: ['hey'], @@ -987,6 +1067,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['bar'], management: { kibana: ['hey'], @@ -1060,6 +1141,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['bar'], management: { kibana: ['hey'], @@ -1101,6 +1183,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['bar'], management: { kibana: ['hey', 'hey-there'], @@ -1142,6 +1225,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'my reserved privileges', @@ -1184,6 +1268,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'my reserved privileges', @@ -1216,12 +1301,14 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }; const feature2: KibanaFeatureConfig = { id: 'test-feature-2', name: 'Test Feature 2', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }; @@ -1346,6 +1433,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }; @@ -1371,6 +1459,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', name: 'Test Feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }; diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 06a3eb158d99d..c6ec2d52c6d1a 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -28,6 +28,14 @@ const managementSchema = Joi.object().pattern( const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)); const alertingSchema = Joi.array().items(Joi.string()); +const appCategorySchema = Joi.object({ + id: Joi.string().required(), + label: Joi.string().required(), + ariaLabel: Joi.string(), + euiIconType: Joi.string(), + order: Joi.number(), +}).required(); + const kibanaPrivilegeSchema = Joi.object({ excludeFromBasePrivileges: Joi.boolean(), management: managementSchema, @@ -80,6 +88,7 @@ const kibanaFeatureSchema = Joi.object({ .invalid(...prohibitedFeatureIds) .required(), name: Joi.string().required(), + category: appCategorySchema, order: Joi.number(), excludeFromBasePrivileges: Joi.boolean(), validLicenses: Joi.array().items( diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 3ff6b1b7bf44f..4cec44d6fa19a 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; import { KibanaFeatureConfig } from '../common'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; export interface BuildOSSFeaturesParams { savedObjectTypes: string[]; @@ -19,6 +20,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS defaultMessage: 'Discover', }), order: 100, + category: DEFAULT_APP_CATEGORIES.kibana, icon: 'discoverApp', navLinkId: 'discover', app: ['discover', 'kibana'], @@ -78,7 +80,8 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS name: i18n.translate('xpack.features.visualizeFeatureName', { defaultMessage: 'Visualize', }), - order: 200, + order: 700, + category: DEFAULT_APP_CATEGORIES.kibana, icon: 'visualizeApp', navLinkId: 'visualize', app: ['visualize', 'lens', 'kibana'], @@ -138,7 +141,8 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS name: i18n.translate('xpack.features.dashboardFeatureName', { defaultMessage: 'Dashboard', }), - order: 300, + order: 200, + category: DEFAULT_APP_CATEGORIES.kibana, icon: 'dashboardApp', navLinkId: 'dashboards', app: ['dashboards', 'kibana'], @@ -217,6 +221,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS defaultMessage: 'Dev Tools', }), order: 1300, + category: DEFAULT_APP_CATEGORIES.management, icon: 'devToolsApp', navLinkId: 'dev_tools', app: ['dev_tools', 'kibana'], @@ -254,6 +259,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS defaultMessage: 'Advanced Settings', }), order: 1500, + category: DEFAULT_APP_CATEGORIES.management, icon: 'advancedSettingsApp', app: ['kibana'], catalogue: ['advanced_settings'], @@ -293,6 +299,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS defaultMessage: 'Index Pattern Management', }), order: 1600, + category: DEFAULT_APP_CATEGORIES.management, icon: 'indexPatternApp', app: ['kibana'], catalogue: ['indexPatterns'], @@ -332,6 +339,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS defaultMessage: 'Saved Objects Management', }), order: 1700, + category: DEFAULT_APP_CATEGORIES.management, icon: 'savedObjectsApp', app: ['kibana'], catalogue: ['saved_objects'], @@ -375,6 +383,7 @@ const timelionFeature: KibanaFeatureConfig = { id: 'timelion', name: 'Timelion', order: 350, + category: DEFAULT_APP_CATEGORIES.kibana, icon: 'timelionApp', navLinkId: 'timelion', app: ['timelion', 'kibana'], diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index ee11e0e2bbe2e..ce6fb548ae6d2 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -35,6 +35,7 @@ describe('Features Plugin', () => { id: 'baz', name: 'baz', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }); @@ -63,6 +64,7 @@ describe('Features Plugin', () => { id: 'baz', name: 'baz', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }); diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts index 30aa6d07f6b5a..692a889203131 100644 --- a/x-pack/plugins/features/server/routes/index.test.ts +++ b/x-pack/plugins/features/server/routes/index.test.ts @@ -28,6 +28,7 @@ describe('GET /api/features', () => { id: 'feature_1', name: 'Feature 1', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }); @@ -36,6 +37,7 @@ describe('GET /api/features', () => { name: 'Feature 2', order: 2, app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }); @@ -44,6 +46,7 @@ describe('GET /api/features', () => { name: 'Feature 2', order: 1, app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }); @@ -51,6 +54,7 @@ describe('GET /api/features', () => { id: 'licensed_feature', name: 'Licensed Feature', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, validLicenses: ['gold'], privileges: null, }); diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts index 7532bc0573b08..f5ba17a632c92 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts @@ -46,6 +46,7 @@ describe('populateUICapabilities', () => { id: 'newFeature', name: 'my new feature', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, privileges: { all: createKibanaFeaturePrivilege(), read: createKibanaFeaturePrivilege(), @@ -93,6 +94,7 @@ describe('populateUICapabilities', () => { name: 'my new feature', navLinkId: 'newFeatureNavLink', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, privileges: { all: createKibanaFeaturePrivilege(['capability1', 'capability2']), read: createKibanaFeaturePrivilege(), @@ -146,6 +148,7 @@ describe('populateUICapabilities', () => { name: 'my new feature', navLinkId: 'newFeatureNavLink', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, catalogue: ['anotherFooEntry', 'anotherBarEntry'], privileges: { all: createKibanaFeaturePrivilege(['capability1', 'capability2']), @@ -215,6 +218,7 @@ describe('populateUICapabilities', () => { name: 'my new feature', navLinkId: 'newFeatureNavLink', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, privileges: { all: createKibanaFeaturePrivilege(['capability1', 'capability2']), read: createKibanaFeaturePrivilege(['capability3', 'capability4', 'capability5']), @@ -245,6 +249,7 @@ describe('populateUICapabilities', () => { name: 'my new feature', navLinkId: 'newFeatureNavLink', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: '', @@ -289,6 +294,7 @@ describe('populateUICapabilities', () => { name: 'my new feature', navLinkId: 'newFeatureNavLink', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, privileges: { all: createKibanaFeaturePrivilege(['capability1', 'capability2']), read: createKibanaFeaturePrivilege(['capability3', 'capability4']), @@ -360,6 +366,7 @@ describe('populateUICapabilities', () => { name: 'my new feature', navLinkId: 'newFeatureNavLink', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, privileges: { all: createKibanaFeaturePrivilege(['capability1', 'capability2']), read: createKibanaFeaturePrivilege(['capability3', 'capability4']), @@ -369,6 +376,7 @@ describe('populateUICapabilities', () => { id: 'anotherNewFeature', name: 'another new feature', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, privileges: { all: createKibanaFeaturePrivilege(['capability1', 'capability2']), read: createKibanaFeaturePrivilege(['capability3', 'capability4']), @@ -379,6 +387,7 @@ describe('populateUICapabilities', () => { name: 'yet another new feature', navLinkId: 'yetAnotherNavLink', app: ['bar-app'], + category: { id: 'foo', label: 'foo' }, privileges: { all: createKibanaFeaturePrivilege(['capability1', 'capability2']), read: createKibanaFeaturePrivilege(['something1', 'something2', 'something3']), diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap index 7bb9954fa3048..b93e27efccaef 100644 --- a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap +++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap @@ -13,7 +13,7 @@ Array [ }, ], "prepend": undefined, - "title": "Canvas • Kibana", + "title": "Canvas • Kibana", "url": "/app/test/Canvas", }, Object { @@ -27,7 +27,7 @@ Array [ }, ], "prepend": undefined, - "title": "Discover • Kibana", + "title": "Discover • Kibana", "url": "/app/test/Discover", }, Object { @@ -41,7 +41,7 @@ Array [ }, ], "prepend": undefined, - "title": "Graph • Kibana", + "title": "Graph • Kibana", "url": "/app/test/Graph", }, ] @@ -60,7 +60,7 @@ Array [ }, ], "prepend": undefined, - "title": "Discover • Kibana", + "title": "Discover • Kibana", "url": "/app/test/Discover", }, Object { @@ -74,7 +74,7 @@ Array [ }, ], "prepend": undefined, - "title": "My Dashboard • Test", + "title": "My Dashboard • Test", "url": "/app/test/My Dashboard", }, ] diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index d69c592655fb5..21c50bf82f4bc 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { Plugin, CoreSetup, CoreStart } from 'src/core/server'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { LicenseState } from './lib/license_state'; import { registerSearchRoute } from './routes/search'; @@ -46,7 +47,8 @@ export class GraphPlugin implements Plugin { name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', { defaultMessage: 'Graph', }), - order: 1200, + order: 600, + category: DEFAULT_APP_CATEGORIES.kibana, icon: 'graphApp', navLinkId: 'graph', app: ['graph', 'kibana'], 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 3867c30655379..b0df3723ca77e 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 @@ -14,8 +14,8 @@ import { SinonFakeServer } from 'sinon'; import { ReactWrapper } from 'enzyme'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; +import { createMemoryHistory } from 'history'; -import { init as initHttpRequests } from './helpers/http_requests'; import { notificationServiceMock, fatalErrorsServiceMock, @@ -41,8 +41,7 @@ import { policyNameAlreadyUsedErrorMessage, maximumDocumentsRequiredMessage, } from '../../public/application/services/policies/policy_validation'; -import { HttpResponse } from './helpers/http_requests'; -import { createMemoryHistory } from 'history'; +import { editPolicyHelpers } from './helpers'; // @ts-ignore initHttp(axios.create({ adapter: axiosXhrAdapter })); @@ -54,11 +53,8 @@ initNotification( const history = createMemoryHistory(); let server: SinonFakeServer; -let httpRequestsMockHelpers: { - setPoliciesResponse: (response: HttpResponse) => void; - setNodesListResponse: (response: HttpResponse) => void; - setNodesDetailsResponse: (nodeAttributes: string, response: HttpResponse) => void; -}; +let httpRequestsMockHelpers: editPolicyHelpers.EditPolicySetup['http']['httpRequestsMockHelpers']; +let http: editPolicyHelpers.EditPolicySetup['http']; const policy = { phases: { hot: { @@ -94,6 +90,17 @@ const activatePhase = async (rendered: ReactWrapper, phase: string) => { }); rendered.update(); }; +const openNodeAttributesSection = (rendered: ReactWrapper, phase: string) => { + const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`); + act(() => { + findTestSubject(getControls(), 'dataTierSelect').simulate('click'); + }); + rendered.update(); + act(() => { + findTestSubject(getControls(), 'customDataAllocationOption').simulate('click'); + }); + rendered.update(); +}; const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[]) => { const errorMessages = rendered.find('.euiFormErrorText'); expect(errorMessages.length).toBe(expectedMessages.length); @@ -119,12 +126,16 @@ const setPolicyName = (rendered: ReactWrapper, policyName: string) => { policyNameField.simulate('change', { target: { value: policyName } }); rendered.update(); }; -const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string) => { +const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string | number) => { const afterInput = rendered.find(`input#${phase}-selectedMinimumAge`); afterInput.simulate('change', { target: { value: after } }); rendered.update(); }; -const setPhaseIndexPriority = (rendered: ReactWrapper, phase: string, priority: string) => { +const setPhaseIndexPriority = ( + rendered: ReactWrapper, + phase: string, + priority: string | number +) => { const priorityInput = rendered.find(`input#${phase}-phaseIndexPriority`); priorityInput.simulate('change', { target: { value: priority } }); rendered.update(); @@ -139,7 +150,9 @@ describe('edit policy', () => { component = ( ); - ({ server, httpRequestsMockHelpers } = initHttpRequests()); + + ({ http } = editPolicyHelpers.setup()); + ({ server, httpRequestsMockHelpers } = http); httpRequestsMockHelpers.setPoliciesResponse(policies); }); @@ -321,7 +334,7 @@ describe('edit policy', () => { describe('warm phase', () => { beforeEach(() => { server.respondImmediately = true; - httpRequestsMockHelpers.setNodesListResponse({}); + http.setupNodeListResponse(); httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, ]); @@ -431,34 +444,39 @@ describe('edit policy', () => { expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); }); test('should show warning instead of node attributes input when none exist', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy(); + openNodeAttributesSection(rendered, 'warm'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); }); test('should show node attributes input when attributes exist', async () => { - httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'warm'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(nodeAttributesSelect.find('option').length).toBe(2); }); test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'warm'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); @@ -473,11 +491,23 @@ describe('edit policy', () => { rendered.update(); expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); }); + test('should show default allocation warning when no node roles are found', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: {}, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'warm'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); + }); }); describe('cold phase', () => { beforeEach(() => { server.respondImmediately = true; - httpRequestsMockHelpers.setNodesListResponse({}); + http.setupNodeListResponse(); httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, ]); @@ -511,34 +541,39 @@ describe('edit policy', () => { expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); }); test('should show warning instead of node attributes input when none exist', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeTruthy(); + openNodeAttributesSection(rendered, 'cold'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); }); test('should show node attributes input when attributes exist', async () => { - httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'cold'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(nodeAttributesSelect.find('option').length).toBe(2); }); test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - httpRequestsMockHelpers.setNodesListResponse({ 'attribute:true': ['node1'] }); const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'cold'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); @@ -563,6 +598,128 @@ describe('edit policy', () => { save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); + test('should show default allocation warning when no node roles are found', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: {}, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'cold'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); + }); + }); + describe('frozen phase', () => { + beforeEach(() => { + server.respondImmediately = true; + http.setupNodeListResponse(); + httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + }); + test('should allow 0 for phase timing', async () => { + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + setPhaseAfter(rendered, 'frozen', 0); + save(rendered); + expectedErrorMessages(rendered, []); + }); + test('should show positive number required error when trying to save cold phase with -1 for after', async () => { + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + setPhaseAfter(rendered, 'frozen', -1); + save(rendered); + expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + }); + test('should show spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(getNodeAttributeSelect(rendered, 'frozen').exists()).toBeFalsy(); + }); + test('should show warning instead of node attributes input when none exist', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'frozen'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); + expect(getNodeAttributeSelect(rendered, 'frozen').exists()).toBeFalsy(); + }); + test('should show node attributes input when attributes exist', async () => { + http.setupNodeListResponse(); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'frozen'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); + const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'frozen'); + expect(nodeAttributesSelect.exists()).toBeTruthy(); + expect(nodeAttributesSelect.find('option').length).toBe(2); + }); + test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { + http.setupNodeListResponse(); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + openNodeAttributesSection(rendered, 'frozen'); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); + const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'frozen'); + expect(nodeAttributesSelect.exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'frozen-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); + expect(nodeAttributesSelect.find('option').length).toBe(2); + nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); + rendered.update(); + const flyoutButton = findTestSubject(rendered, 'frozen-viewNodeDetailsFlyoutButton'); + expect(flyoutButton.exists()).toBeTruthy(); + await act(async () => { + await flyoutButton.simulate('click'); + }); + rendered.update(); + expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); + }); + test('should show positive number required error when trying to save with -1 for index priority', async () => { + http.setupNodeListResponse(); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + setPhaseAfter(rendered, 'frozen', 1); + setPhaseIndexPriority(rendered, 'frozen', -1); + save(rendered); + expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + }); + test('should show default allocation warning when no node roles are found', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: {}, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'frozen'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); + }); }); describe('delete phase', () => { test('should allow 0 for phase timing', async () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts new file mode 100644 index 0000000000000..4eeb542671d23 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { init as initHttpRequests } from './http_requests'; + +export type EditPolicySetup = ReturnType; + +export const setup = () => { + const { httpRequestsMockHelpers, server } = initHttpRequests(); + + const setupNodeListResponse = ( + response: Record = { + nodesByAttributes: { 'attribute:true': ['node1'] }, + nodesByRoles: { data: ['node1'] }, + } + ) => { + httpRequestsMockHelpers.setNodesListResponse(response); + }; + + return { + http: { + setupNodeListResponse, + httpRequestsMockHelpers, + server, + }, + }; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts index 6cbe3bdf1f8c6..a9d326073e4d3 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts @@ -40,6 +40,8 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; }; +export type HttpRequestMockHelpers = ReturnType; + export const init = () => { const server = sinon.fakeServer.create(); diff --git a/x-pack/legacy/plugins/spaces/server/lib/errors.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts similarity index 57% rename from x-pack/legacy/plugins/spaces/server/lib/errors.ts rename to x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts index 4d8d71dca7af6..4c32ea121bb57 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/errors.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { boomify, isBoom } from 'boom'; +import * as editPolicyHelpers from './edit_policy'; -export function wrapError(error: any) { - if (isBoom(error)) { - return error; - } +export { HttpRequestMockHelpers, init } from './http_requests'; - return boomify(error, { statusCode: error.status }); -} +export { editPolicyHelpers }; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts new file mode 100644 index 0000000000000..16b8fbd127ab6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type NodeDataRole = 'data' | 'data_hot' | 'data_warm' | 'data_cold' | 'data_frozen'; + +export interface ListNodesRouteResponse { + nodesByAttributes: { [attributePair: string]: string[] }; + nodesByRoles: { [role in NodeDataRole]?: string[] }; +} diff --git a/x-pack/plugins/index_lifecycle_management/common/types/index.ts b/x-pack/plugins/index_lifecycle_management/common/types/index.ts index fef79c7782bb0..a23dc647f1f65 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/index.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './api'; + export * from './policies'; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 97effee44533a..8f913dd884dfe 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -6,6 +6,8 @@ import { Index as IndexInterface } from '../../../index_management/common/types'; +export type PhaseWithAllocation = 'warm' | 'cold' | 'frozen'; + export interface SerializedPolicy { name: string; phases: Phases; @@ -62,6 +64,7 @@ export interface SerializedWarmPhase extends SerializedPhase { set_priority?: { priority: number | null; }; + migrate?: { enabled: boolean }; }; } @@ -72,6 +75,7 @@ export interface SerializedColdPhase extends SerializedPhase { set_priority?: { priority: number | null; }; + migrate?: { enabled: boolean }; }; } @@ -82,6 +86,7 @@ export interface SerializedFrozenPhase extends SerializedPhase { set_priority?: { priority: number | null; }; + migrate?: { enabled: boolean }; }; } @@ -103,6 +108,13 @@ export interface AllocateAction { require?: { [attribute: string]: string; }; + migrate?: { + /** + * If enabled is ever set it will only be set to `false` because the default value + * for this is `true`. Rather leave unspecified for true. + */ + enabled: false; + }; } export interface Policy { @@ -125,9 +137,23 @@ export interface PhaseWithMinAge { selectedMinimumAgeUnits: string; } +/** + * Different types of allocation markers we use in deserialized policies. + * + * default - use data tier based data allocation based on node roles -- this is ES best practice mode. + * custom - use node_attrs to allocate data to specific nodes + * none - do not move data anywhere when entering a phase + */ +export type DataTierAllocationType = 'default' | 'custom' | 'none'; + export interface PhaseWithAllocationAction { selectedNodeAttrs: string; selectedReplicaCount: string; + /** + * A string value indicating allocation type. If unspecified we assume the user + * wants to use default allocation. + */ + dataTierAllocationType: DataTierAllocationType; } export interface PhaseWithIndexPriority { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index f11860d36faf8..6d4c57d23138d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -38,6 +38,7 @@ export const defaultNewWarmPhase: WarmPhase = { selectedReplicaCount: '', warmPhaseOnRollover: true, phaseIndexPriority: '50', + dataTierAllocationType: 'default', }; export const defaultNewColdPhase: ColdPhase = { @@ -48,6 +49,7 @@ export const defaultNewColdPhase: ColdPhase = { selectedReplicaCount: '', freezeEnabled: false, phaseIndexPriority: '0', + dataTierAllocationType: 'default', }; export const defaultNewFrozenPhase: FrozenPhase = { @@ -58,6 +60,7 @@ export const defaultNewFrozenPhase: FrozenPhase = { selectedReplicaCount: '', freezeEnabled: false, phaseIndexPriority: '0', + dataTierAllocationType: 'default', }; export const defaultNewDeletePhase: DeletePhase = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/check_phase_compatibility.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/check_phase_compatibility.ts new file mode 100644 index 0000000000000..2ef0fb145551f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/check_phase_compatibility.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + NodeDataRole, + ListNodesRouteResponse, + PhaseWithAllocation, +} from '../../../../common/types'; + +/** + * Given a phase and current node roles, determine whether the phase + * can use default data tier allocation. + * + * This can only be checked for phases that have an allocate action. + */ +export const isPhaseDefaultDataAllocationCompatible = ( + phase: PhaseWithAllocation, + nodesByRoles: ListNodesRouteResponse['nodesByRoles'] +): boolean => { + // The 'data' role covers all node roles, so if we have at least one node with the data role + // we can use default allocation. + if (nodesByRoles.data?.length) { + return true; + } + + // Otherwise we need to check whether a node role for the specific phase exists + if (nodesByRoles[`data_${phase}` as NodeDataRole]?.length) { + return true; + } + + // Otherwise default allocation has nowhere to allocate new shards to in this phase. + return false; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts new file mode 100644 index 0000000000000..4067ad97fc43b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DataTierAllocationType, AllocateAction } from '../../../../common/types'; + +/** + * Determine what deserialized state the policy config represents. + * + * See {@DataTierAllocationType} for more information. + */ +export const determineDataTierAllocationType = ( + allocateAction?: AllocateAction +): DataTierAllocationType => { + if (!allocateAction) { + return 'default'; + } + + if (allocateAction.migrate?.enabled === false) { + return 'none'; + } + + if ( + (allocateAction.require && Object.keys(allocateAction.require).length) || + (allocateAction.include && Object.keys(allocateAction.include).length) || + (allocateAction.exclude && Object.keys(allocateAction.exclude).length) + ) { + return 'custom'; + } + + return 'default'; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/index.ts new file mode 100644 index 0000000000000..67a512cefe00c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './determine_allocation_type'; + +export * from './check_phase_compatibility'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts new file mode 100644 index 0000000000000..1dabae1a0f0c4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './data_tiers'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss new file mode 100644 index 0000000000000..62ec3f303e1e8 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss @@ -0,0 +1,9 @@ +.indexLifecycleManagement__phase__dataTierAllocation { + &__controlSection { + background-color: $euiColorLightestShade; + padding-top: $euiSizeM; + padding-left: $euiSizeM; + padding-right: $euiSizeM; + padding-bottom: $euiSizeM; + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx new file mode 100644 index 0000000000000..3ae60a5a3d622 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiText, EuiFormRow, EuiSpacer, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; + +import { DataTierAllocationType } from '../../../../../../common/types'; +import { NodeAllocation } from './node_allocation'; +import { SharedProps } from './types'; + +import './data_tier_allocation.scss'; + +type SelectOptions = EuiSuperSelectOption; + +const i18nTexts = { + allocationFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.allocationFieldLabel', + { defaultMessage: 'Data tier options' } + ), + allocationOptions: { + warm: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input', + { defaultMessage: 'Use warm nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the warm tier.' } + ), + }, + none: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.input', + { defaultMessage: 'Off' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText', + { defaultMessage: 'Do not move data in the warm phase.' } + ), + }, + custom: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), + }, + }, + cold: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input', + { defaultMessage: 'Use cold nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the cold tier.' } + ), + }, + none: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input', + { defaultMessage: 'Off' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText', + { defaultMessage: 'Do not move data in the cold phase.' } + ), + }, + custom: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), + }, + }, + frozen: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.input', + { defaultMessage: 'Use frozen nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the frozen tier.' } + ), + }, + none: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.input', + { defaultMessage: 'Off' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.helpText', + { defaultMessage: 'Do not move data in the frozen phase.' } + ), + }, + custom: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), + }, + }, + }, +}; + +export const DataTierAllocation: FunctionComponent = (props) => { + const { phaseData, setPhaseData, phase, hasNodeAttributes } = props; + + return ( +
+ + setPhaseData('dataTierAllocationType', value)} + options={ + [ + { + value: 'default', + inputDisplay: i18nTexts.allocationOptions[phase].default.input, + dropdownDisplay: ( + <> + {i18nTexts.allocationOptions[phase].default.input} + +

+ {i18nTexts.allocationOptions[phase].default.helpText} +

+
+ + ), + }, + { + value: 'none', + inputDisplay: i18nTexts.allocationOptions[phase].none.inputDisplay, + dropdownDisplay: ( + <> + {i18nTexts.allocationOptions[phase].none.inputDisplay} + +

+ {i18nTexts.allocationOptions[phase].none.helpText} +

+
+ + ), + }, + { + 'data-test-subj': 'customDataAllocationOption', + value: 'custom', + inputDisplay: i18nTexts.allocationOptions[phase].custom.inputDisplay, + dropdownDisplay: ( + <> + {i18nTexts.allocationOptions[phase].custom.inputDisplay} + +

+ {i18nTexts.allocationOptions[phase].custom.helpText} +

+
+ + ), + }, + ] as SelectOptions[] + } + /> +
+ {phaseData.dataTierAllocationType === 'custom' && hasNodeAttributes && ( + <> + +
+ +
+ + )} +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_warning.tsx new file mode 100644 index 0000000000000..a7ebc0d2e4a24 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_warning.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; + +import { PhaseWithAllocation } from '../../../../../../common/types'; + +const i18nTexts = { + warm: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the warm tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the warm tier to use role-based allocation. The policy will fail to complete allocation if there are no warm nodes.', + } + ), + }, + cold: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the cold tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the cold tier to use role-based allocation. The policy will fail to complete allocation if there are no cold nodes.', + } + ), + }, + frozen: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the frozen tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the frozen tier to use role-based allocation. The policy will fail to complete allocation if there are no frozen nodes.', + } + ), + }, +}; + +interface Props { + phase: PhaseWithAllocation; +} + +export const DefaultAllocationWarning: FunctionComponent = ({ phase }) => { + return ( + <> + + + {i18nTexts[phase].body} + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts new file mode 100644 index 0000000000000..26464a75ae14c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { NodesDataProvider } from './node_data_provider'; +export { NodeAllocation } from './node_allocation'; +export { NodeAttrsDetails } from './node_attrs_details'; +export { DataTierAllocation } from './data_tier_allocation'; +export { DefaultAllocationWarning } from './default_allocation_warning'; +export { NoNodeAttributesWarning } from './no_node_attributes_warning'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx new file mode 100644 index 0000000000000..1ba82623c2b94 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { PhaseWithAllocation } from '../../../../../../common/types'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel', { + defaultMessage: 'No custom node attributes configured', + }), + warm: { + body: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription', + { + defaultMessage: + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Warm nodes will be used instead.', + } + ), + }, + cold: { + body: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription', + { + defaultMessage: + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Cold nodes will be used instead.', + } + ), + }, + frozen: { + body: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozen.nodeAttributesMissingDescription', + { + defaultMessage: + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Frozen nodes will be used instead.', + } + ), + }, +}; + +export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAllocation }> = ({ + phase, +}) => { + return ( + <> + + + {i18nTexts[phase].body} + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx new file mode 100644 index 0000000000000..a57a6ba4ff2c6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiSelect, EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui'; + +import { PhaseWithAllocationAction } from '../../../../../../common/types'; +import { propertyof } from '../../../../services/policies/policy_validation'; + +import { ErrableFormRow } from '../form_errors'; + +import { NodeAttrsDetails } from './node_attrs_details'; +import { SharedProps } from './types'; +import { LearnMoreLink } from '../learn_more_link'; + +const learnMoreLink = ( + + } + docPath="modules-cluster.html#cluster-shard-allocation-settings" + /> +); + +const i18nTexts = { + doNotModifyAllocationOption: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.nodeAllocation.doNotModifyAllocationOption', + { defaultMessage: 'Do not modify allocation configuration' } + ), +}; + +export const NodeAllocation: FunctionComponent = ({ + phase, + setPhaseData, + errors, + phaseData, + isShowingErrors, + nodes, +}) => { + const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( + null + ); + + const nodeOptions = Object.keys(nodes).map((attrs) => ({ + text: `${attrs} (${nodes[attrs].length})`, + value: attrs, + })); + + nodeOptions.sort((a, b) => a.value.localeCompare(b.value)); + + // check that this string is a valid property + const nodeAttrsProperty = propertyof('selectedNodeAttrs'); + + return ( + <> + +

+ +

+
+ + + {/* + TODO: this field component must be revisited to support setting multiple require values and to support + setting `include and exclude values on ILM policies. See https://github.com/elastic/kibana/issues/77344 + */} + setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)} + > + + + ) : null + } + > + { + setPhaseData(nodeAttrsProperty, e.target.value); + }} + /> + + + {selectedNodeAttrsForDetails ? ( + setSelectedNodeAttrsForDetails(null)} + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx similarity index 97% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx index af8833c8082b3..c29495d13eb8e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_attrs_details.tsx @@ -20,7 +20,7 @@ import { EuiButton, } from '@elastic/eui'; -import { useLoadNodeDetails } from '../../../services/api'; +import { useLoadNodeDetails } from '../../../../services/api'; interface Props { close: () => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx new file mode 100644 index 0000000000000..a7c0f3ec7c866 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { ListNodesRouteResponse } from '../../../../../../common/types'; +import { useLoadNodes } from '../../../../services/api'; + +interface Props { + children: (data: ListNodesRouteResponse) => JSX.Element; +} + +export const NodesDataProvider = ({ children }: Props): JSX.Element => { + const { isLoading, data, error, resendRequest } = useLoadNodes(); + + if (isLoading) { + return ( + <> + + + + ); + } + + const renderError = () => { + if (error) { + const { statusCode, message } = error; + return ( + <> + + } + color="danger" + > +

+ {message} ({statusCode}) +

+ + + +
+ + + + ); + } + return null; + }; + + return ( + <> + {renderError()} + {/* `data` will always be defined because we use an initial value when loading */} + {children(data!)} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts new file mode 100644 index 0000000000000..d4cb31a3be9e7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ListNodesRouteResponse, + PhaseWithAllocation, + PhaseWithAllocationAction, +} from '../../../../../../common/types'; +import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; + +export interface SharedProps { + phase: PhaseWithAllocation; + errors?: PhaseValidationErrors; + phaseData: PhaseWithAllocationAction; + setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; + isShowingErrors: boolean; + nodes: ListNodesRouteResponse['nodesByAttributes']; + hasNodeAttributes: boolean; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_field.tsx new file mode 100644 index 0000000000000..7bf8cd3ba6d90 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_field.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiDescribedFormGroup, EuiDescribedFormGroupProps } from '@elastic/eui'; + +import { ToggleableField, Props as ToggleableFieldProps } from './toggleable_field'; + +type Props = EuiDescribedFormGroupProps & { + switchProps: ToggleableFieldProps; +}; + +export const DescribedFormField: FunctionComponent = ({ + children, + switchProps, + ...restDescribedFormProps +}) => { + return ( + + {children} + + ); +}; 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 4410c4bb38397..2428cade0898e 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 @@ -8,11 +8,17 @@ export { ActiveBadge } from './active_badge'; export { ErrableFormRow } from './form_errors'; export { LearnMoreLink } from './learn_more_link'; export { MinAgeInput } from './min_age_input'; -export { NodeAllocation } from './node_allocation'; -export { NodeAttrsDetails } from './node_attrs_details'; export { OptionalLabel } from './optional_label'; export { PhaseErrorMessage } from './phase_error_message'; export { PolicyJsonFlyout } from './policy_json_flyout'; export { SetPriorityInput } from './set_priority_input'; export { SnapshotPolicies } from './snapshot_policies'; +export { + DataTierAllocation, + NodeAllocation, + NodeAttrsDetails, + NodesDataProvider, + DefaultAllocationWarning, +} from './data_tier_allocation'; +export { DescribedFormField } from './described_form_field'; export { Forcemerge } from './forcemerge'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx deleted file mode 100644 index 6a22d8716514c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiSelect, - EuiButtonEmpty, - EuiCallOut, - EuiSpacer, - EuiLoadingSpinner, - EuiButton, -} from '@elastic/eui'; - -import { LearnMoreLink } from './learn_more_link'; -import { ErrableFormRow } from './form_errors'; -import { useLoadNodes } from '../../../services/api'; -import { NodeAttrsDetails } from './node_attrs_details'; -import { PhaseWithAllocationAction, Phases } from '../../../../../common/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; - -const learnMoreLink = ( - - - - } - docPath="modules-cluster.html#cluster-shard-allocation-settings" - /> - -); - -interface Props { - phase: keyof Phases & string; - errors?: PhaseValidationErrors; - phaseData: T; - setPhaseData: (dataKey: keyof T & string, value: string) => void; - isShowingErrors: boolean; -} -export const NodeAllocation = ({ - phase, - setPhaseData, - errors, - phaseData, - isShowingErrors, -}: React.PropsWithChildren>) => { - const { isLoading, data: nodes, error, resendRequest } = useLoadNodes(); - - const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( - null - ); - - if (isLoading) { - return ( - - - - - ); - } - - if (error) { - const { statusCode, message } = error; - return ( - - - } - color="danger" - > -

- {message} ({statusCode}) -

- - - -
- - -
- ); - } - - let nodeOptions = Object.keys(nodes).map((attrs) => ({ - text: `${attrs} (${nodes[attrs].length})`, - value: attrs, - })); - - nodeOptions.sort((a, b) => a.value.localeCompare(b.value)); - if (nodeOptions.length) { - nodeOptions = [ - { - text: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.defaultNodeAllocation', { - defaultMessage: "Default allocation (don't use attributes)", - }), - value: '', - }, - ...nodeOptions, - ]; - } - if (!nodeOptions.length) { - return ( - - - } - color="warning" - > - - {learnMoreLink} - - - - - ); - } - - // check that this string is a valid property - const nodeAttrsProperty = propertyof('selectedNodeAttrs'); - - return ( - - - { - setPhaseData(nodeAttrsProperty, e.target.value); - }} - /> - - {!!phaseData.selectedNodeAttrs ? ( - setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)} - > - - - ) : null} - {learnMoreLink} - - - {selectedNodeAttrsForDetails ? ( - setSelectedNodeAttrsForDetails(null)} - /> - ) : null} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx new file mode 100644 index 0000000000000..ff4301808db33 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useState } from 'react'; +import { EuiSpacer, EuiSwitch, EuiSwitchProps } from '@elastic/eui'; + +export interface Props extends Omit { + initialValue: boolean; + onChange: (nextValue: boolean) => void; +} + +export const ToggleableField: FunctionComponent = ({ + initialValue, + onChange, + children, + ...restProps +}) => { + const [isContentVisible, setIsContentVisible] = useState(initialValue); + + return ( + <> + { + const nextValue = e.target.checked; + setIsContentVisible(nextValue); + onChange(nextValue); + }} + /> + + {isContentVisible ? children : null} + + ); +}; 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 f1c287788e08d..85529ef0c9a5b 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { Fragment, useEffect, useState, useCallback } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -45,7 +45,7 @@ import { import { ErrableFormRow, LearnMoreLink, PolicyJsonFlyout } from './components'; import { ColdPhase, DeletePhase, FrozenPhase, HotPhase, WarmPhase } from './phases'; -interface Props { +export interface Props { policies: PolicyFromES[]; policyName: string; getUrlForApp: ( @@ -119,15 +119,39 @@ export const EditPolicy: React.FunctionComponent = ({ setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout); }; - const setPhaseData = (phase: keyof Phases, key: string, value: any) => { - setPolicy({ - ...policy, - phases: { - ...policy.phases, - [phase]: { ...policy.phases[phase], [key]: value }, - }, - }); - }; + const setPhaseData = useCallback( + (phase: keyof Phases, key: string, value: any) => { + setPolicy((nextPolicy) => ({ + ...nextPolicy, + phases: { + ...nextPolicy.phases, + [phase]: { ...nextPolicy.phases[phase], [key]: value }, + }, + })); + }, + [setPolicy] + ); + + const setHotPhaseData = useCallback( + (key: string, value: any) => setPhaseData('hot', key, value), + [setPhaseData] + ); + const setWarmPhaseData = useCallback( + (key: string, value: any) => setPhaseData('warm', key, value), + [setPhaseData] + ); + const setColdPhaseData = useCallback( + (key: string, value: any) => setPhaseData('cold', key, value), + [setPhaseData] + ); + const setFrozenPhaseData = useCallback( + (key: string, value: any) => setPhaseData('frozen', key, value), + [setPhaseData] + ); + const setDeletePhaseData = useCallback( + (key: string, value: any) => setPhaseData('delete', key, value), + [setPhaseData] + ); const setWarmPhaseOnRollover = (value: boolean) => { setPolicy({ @@ -277,7 +301,7 @@ export const EditPolicy: React.FunctionComponent = ({ 0} - setPhaseData={(key, value) => setPhaseData('hot', key, value)} + setPhaseData={setHotPhaseData} phaseData={policy.phases.hot} setWarmPhaseOnRollover={setWarmPhaseOnRollover} /> @@ -287,7 +311,7 @@ export const EditPolicy: React.FunctionComponent = ({ 0} - setPhaseData={(key, value) => setPhaseData('warm', key, value)} + setPhaseData={setWarmPhaseData} phaseData={policy.phases.warm} hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} /> @@ -297,7 +321,7 @@ export const EditPolicy: React.FunctionComponent = ({ 0} - setPhaseData={(key, value) => setPhaseData('cold', key, value)} + setPhaseData={setColdPhaseData} phaseData={policy.phases.cold} hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} /> @@ -307,7 +331,7 @@ export const EditPolicy: React.FunctionComponent = ({ 0} - setPhaseData={(key, value) => setPhaseData('frozen', key, value)} + setPhaseData={setFrozenPhaseData} phaseData={policy.phases.frozen} hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} /> @@ -318,7 +342,7 @@ export const EditPolicy: React.FunctionComponent = ({ errors={errors?.delete} isShowingErrors={isShowingErrors && !!errors && Object.keys(errors.delete).length > 0} getUrlForApp={getUrlForApp} - setPhaseData={(key, value) => setPhaseData('delete', key, value)} + setPhaseData={setDeletePhaseData} phaseData={policy.phases.delete} hotPhaseRolloverEnabled={policy.phases.hot.rolloverEnabled} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index ae2858e7a84ae..241a98fffa6df 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -4,19 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { FunctionComponent, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiFieldNumber, - EuiDescribedFormGroup, - EuiSwitch, - EuiTextColor, -} from '@elastic/eui'; +import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui'; import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../common/types'; import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; @@ -27,14 +19,24 @@ import { PhaseErrorMessage, OptionalLabel, ErrableFormRow, - MinAgeInput, - NodeAllocation, SetPriorityInput, + MinAgeInput, + DescribedFormField, } from '../components'; -const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { - defaultMessage: 'Freeze index', -}); +import { DataTierAllocationField } from './shared'; + +const i18nTexts = { + freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', + }), + dataTierAllocation: { + description: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.description', { + defaultMessage: + 'Move data to data nodes optimized for less frequent, read-only access. Store cold data on less-expensive hardware.', + }), + }, +}; const coldProperty: keyof Phases = 'cold'; const phaseProperty = (propertyName: keyof ColdPhaseInterface) => propertyName; @@ -46,18 +48,17 @@ interface Props { errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } -export class ColdPhase extends PureComponent { - render() { - const { - setPhaseData, - phaseData, - errors, - isShowingErrors, - hotPhaseRolloverEnabled, - } = this.props; - - return ( -
+export const ColdPhase: FunctionComponent = ({ + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, +}) => { + return ( +
+ <> + {/* Section title group; containing min age */} @@ -86,7 +87,7 @@ export class ColdPhase extends PureComponent { data-test-subj="enablePhaseSwitch-cold" label={ } @@ -101,68 +102,83 @@ export class ColdPhase extends PureComponent { } fullWidth > - - {phaseData.phaseEnabled ? ( - - - errors={errors} - phaseData={phaseData} - phase={coldProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - rolloverEnabled={hotPhaseRolloverEnabled} - /> - - - - phase={coldProperty} - setPhaseData={setPhaseData} - errors={errors} - phaseData={phaseData} - isShowingErrors={isShowingErrors} - /> - - - - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.freezeEnabled} - helpText={i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText', - { - defaultMessage: 'By default, the number of replicas remains the same.', - } - )} - > - { - setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); - }} - min={0} - /> - - - - - ) : ( -
- )} - + {phaseData.phaseEnabled ? ( + + errors={errors} + phaseData={phaseData} + phase={coldProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + ) : null} {phaseData.phaseEnabled ? ( + {/* Data tier allocation section */} + + + {/* Replicas section */} + + {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean(phaseData.selectedReplicaCount), + onChange: (v) => { + if (!v) { + setPhaseData('selectedReplicaCount', ''); + } + }, + }} + fullWidth + > + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.selectedReplicaCount} + > + { + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); + }} + min={0} + /> + + + {/* Freeze section */} @@ -191,8 +207,8 @@ export class ColdPhase extends PureComponent { onChange={(e) => { setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); }} - label={freezeLabel} - aria-label={freezeLabel} + label={i18nTexts.freezeLabel} + aria-label={i18nTexts.freezeLabel} /> @@ -204,7 +220,7 @@ export class ColdPhase extends PureComponent { /> ) : null} -
- ); - } -} + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx index bfaf141438169..6a849cc2c3f1f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx @@ -4,19 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { FunctionComponent, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiFieldNumber, - EuiDescribedFormGroup, - EuiSwitch, - EuiTextColor, -} from '@elastic/eui'; +import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui'; import { FrozenPhase as FrozenPhaseInterface, Phases } from '../../../../../common/types'; import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; @@ -28,13 +20,22 @@ import { OptionalLabel, ErrableFormRow, MinAgeInput, - NodeAllocation, SetPriorityInput, + DescribedFormField, } from '../components'; +import { DataTierAllocationField } from './shared'; -const freezeLabel = i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', { - defaultMessage: 'Freeze index', -}); +const i18nTexts = { + freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', + }), + dataTierAllocation: { + description: i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.dataTier.description', { + defaultMessage: + 'Move data to data nodes optimized for infrequent, read-only access. Store frozen data on the least-expensive hardware.', + }), + }, +}; const frozenProperty: keyof Phases = 'frozen'; const phaseProperty = (propertyName: keyof FrozenPhaseInterface) => propertyName; @@ -46,18 +47,17 @@ interface Props { errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } -export class FrozenPhase extends PureComponent { - render() { - const { - setPhaseData, - phaseData, - errors, - isShowingErrors, - hotPhaseRolloverEnabled, - } = this.props; - - return ( -
+export const FrozenPhase: FunctionComponent = ({ + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, +}) => { + return ( +
+ <> + {/* Section title group; containing min age */} @@ -101,68 +101,82 @@ export class FrozenPhase extends PureComponent { } fullWidth > - - {phaseData.phaseEnabled ? ( - - - errors={errors} - phaseData={phaseData} - phase={frozenProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - rolloverEnabled={hotPhaseRolloverEnabled} - /> - - - - phase={frozenProperty} - setPhaseData={setPhaseData} - errors={errors} - phaseData={phaseData} - isShowingErrors={isShowingErrors} - /> - - - - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.freezeEnabled} - helpText={i18n.translate( - 'xpack.indexLifecycleMgmt.frozenPhase.replicaCountHelpText', - { - defaultMessage: 'By default, the number of replicas remains the same.', - } - )} - > - { - setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); - }} - min={0} - /> - - - - - ) : ( -
- )} - + {phaseData.phaseEnabled ? ( + + errors={errors} + phaseData={phaseData} + phase={frozenProperty} + isShowingErrors={isShowingErrors} + setPhaseData={setPhaseData} + rolloverEnabled={hotPhaseRolloverEnabled} + /> + ) : null} {phaseData.phaseEnabled ? ( + {/* Data tier allocation section */} + + + {/* Replicas section */} + + {i18n.translate('xpack.indexLifecycleMgmt.frozenPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean(phaseData.selectedReplicaCount), + onChange: (v) => { + if (!v) { + setPhaseData('selectedReplicaCount', ''); + } + }, + }} + fullWidth + > + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.selectedReplicaCount} + > + { + setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); + }} + min={0} + /> + + @@ -191,8 +205,8 @@ export class FrozenPhase extends PureComponent { onChange={(e) => { setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); }} - label={freezeLabel} - aria-label={freezeLabel} + label={i18nTexts.freezeLabel} + aria-label={i18nTexts.freezeLabel} /> @@ -204,7 +218,7 @@ export class FrozenPhase extends PureComponent { /> ) : null} -
- ); - } -} + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx new file mode 100644 index 0000000000000..6475e5286a778 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../common/types'; + +import { + DataTierAllocation, + DefaultAllocationWarning, + NoNodeAttributesWarning, + NodesDataProvider, +} from '../../components/data_tier_allocation'; +import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; +import { isPhaseDefaultDataAllocationCompatible } from '../../../../lib/data_tiers'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', { + defaultMessage: 'Data allocation', + }), +}; + +interface Props { + description: React.ReactNode; + phase: PhaseWithAllocation; + setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; + isShowingErrors: boolean; + errors?: PhaseValidationErrors; + phaseData: PhaseWithAllocationAction; +} + +/** + * Top-level layout control for the data tier allocation field. + */ +export const DataTierAllocationField: FunctionComponent = ({ + description, + phase, + phaseData, + setPhaseData, + isShowingErrors, + errors, +}) => { + return ( + + {(nodesData) => { + const isCompatible = isPhaseDefaultDataAllocationCompatible(phase, nodesData.nodesByRoles); + const hasNodeAttrs = Boolean(Object.keys(nodesData.nodesByAttributes ?? {}).length); + + return ( + {i18nTexts.title}} + description={description} + fullWidth + > + + <> + + + {/* Data tier related warnings */} + + {phaseData.dataTierAllocationType === 'default' && !isCompatible && ( + + )} + + {phaseData.dataTierAllocationType === 'custom' && !hasNodeAttrs && ( + + )} + + + + ); + }} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts new file mode 100644 index 0000000000000..f9e939058adb9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DataTierAllocationField } from './data_tier_allocation_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index c806056899cac..16a740b1171c9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, PureComponent } from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { @@ -27,44 +27,53 @@ import { OptionalLabel, ErrableFormRow, SetPriorityInput, - NodeAllocation, MinAgeInput, + DescribedFormField, Forcemerge, } from '../components'; +import { DataTierAllocationField } from './shared'; -const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { - defaultMessage: 'Shrink index', -}); - -const moveToWarmPhaseOnRolloverLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', - { - defaultMessage: 'Move to warm phase on rollover', - } -); +const i18nTexts = { + shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { + defaultMessage: 'Shrink index', + }), + moveToWarmPhaseOnRolloverLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', + { + defaultMessage: 'Move to warm phase on rollover', + } + ), + dataTierAllocation: { + description: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.description', { + defaultMessage: + 'Move warm data to nodes optimized for read-only access. Store warm data on less-expensive hardware.', + }), + }, +}; const warmProperty: keyof Phases = 'warm'; const phaseProperty = (propertyName: keyof WarmPhaseInterface) => propertyName; interface Props { - setPhaseData: (key: keyof WarmPhaseInterface & string, value: boolean | string) => void; + setPhaseData: ( + key: keyof WarmPhaseInterface & string, + value: boolean | string | undefined + ) => void; phaseData: WarmPhaseInterface; isShowingErrors: boolean; errors?: PhaseValidationErrors; hotPhaseRolloverEnabled: boolean; } -export class WarmPhase extends PureComponent { - render() { - const { - setPhaseData, - phaseData, - errors, - isShowingErrors, - hotPhaseRolloverEnabled, - } = this.props; - - return ( -
+export const WarmPhase: FunctionComponent = ({ + setPhaseData, + phaseData, + errors, + isShowingErrors, + hotPhaseRolloverEnabled, +}) => { + return ( +
+ <> @@ -115,7 +124,7 @@ export class WarmPhase extends PureComponent { { @@ -137,58 +146,75 @@ export class WarmPhase extends PureComponent { /> ) : null} - - - - - phase={warmProperty} - setPhaseData={setPhaseData} - errors={errors} - phaseData={phaseData} - isShowingErrors={isShowingErrors} - /> - - - - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.selectedReplicaCount} - helpText={i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText', - { - defaultMessage: 'By default, the number of replicas remains the same.', - } - )} - > - { - setPhaseData('selectedReplicaCount', e.target.value); - }} - min={0} - /> - - - - - ) : null} + {phaseData.phaseEnabled ? ( + {/* Data tier allocation section */} + + + + {i18n.translate('xpack.indexLifecycleMgmt.warmPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean(phaseData.selectedReplicaCount), + onChange: (v) => { + if (!v) { + setPhaseData('selectedReplicaCount', ''); + } + }, + }} + fullWidth + > + + + + + } + isShowingErrors={isShowingErrors} + errors={errors?.selectedReplicaCount} + > + { + setPhaseData('selectedReplicaCount', e.target.value); + }} + min={0} + /> + + @@ -217,8 +243,8 @@ export class WarmPhase extends PureComponent { onChange={(e) => { setPhaseData(phaseProperty('shrinkEnabled'), e.target.checked); }} - label={shrinkLabel} - aria-label={shrinkLabel} + label={i18nTexts.shrinkLabel} + aria-label={i18nTexts.shrinkLabel} aria-controls="shrinkContent" /> @@ -275,7 +301,7 @@ export class WarmPhase extends PureComponent { /> ) : null} -
- ); - } -} + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index 3d068433becbd..b279a5647c3e8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -6,7 +6,7 @@ import { METRIC_TYPE } from '@kbn/analytics'; -import { PolicyFromES, SerializedPolicy } from '../../../common/types'; +import { PolicyFromES, SerializedPolicy, ListNodesRouteResponse } from '../../../common/types'; import { UIM_POLICY_DELETE, @@ -23,10 +23,10 @@ interface GenericObject { } export const useLoadNodes = () => { - return useRequest({ + return useRequest({ path: `nodes/list`, method: 'get', - initialData: [], + initialData: { nodesByAttributes: {}, nodesByRoles: {} } as ListNodesRouteResponse, }); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts index 3b71c11349752..70f172de390e3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -13,6 +13,8 @@ import { PhaseValidationErrors, positiveNumberRequiredMessage, } from './policy_validation'; +import { determineDataTierAllocationType } from '../../lib'; +import { serializePhaseWithAllocation } from './shared'; const coldPhaseInitialization: ColdPhase = { phaseEnabled: false, @@ -22,6 +24,7 @@ const coldPhaseInitialization: ColdPhase = { selectedReplicaCount: '', freezeEnabled: false, phaseIndexPriority: '', + dataTierAllocationType: 'default', }; export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhase => { @@ -32,6 +35,12 @@ export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhas phase.phaseEnabled = true; + if (phaseSerialized.actions.allocate) { + phase.dataTierAllocationType = determineDataTierAllocationType( + phaseSerialized.actions.allocate + ); + } + if (phaseSerialized.min_age) { const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); phase.selectedMinimumAge = minAge; @@ -80,19 +89,7 @@ export const coldPhaseToES = ( esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; } - esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; - - if (phase.selectedNodeAttrs) { - const [name, value] = phase.selectedNodeAttrs.split(':'); - esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); - esPhase.actions.allocate.require = { - [name]: value, - }; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.require; - } - } + esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions); if (isNumber(phase.selectedReplicaCount)) { esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts index 6249507bcb407..28d18b8f89263 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts @@ -13,6 +13,8 @@ import { PhaseValidationErrors, positiveNumberRequiredMessage, } from './policy_validation'; +import { determineDataTierAllocationType } from '../../lib'; +import { serializePhaseWithAllocation } from './shared'; const frozenPhaseInitialization: FrozenPhase = { phaseEnabled: false, @@ -22,6 +24,7 @@ const frozenPhaseInitialization: FrozenPhase = { selectedReplicaCount: '', freezeEnabled: false, phaseIndexPriority: '', + dataTierAllocationType: 'default', }; export const frozenPhaseFromES = (phaseSerialized?: SerializedFrozenPhase): FrozenPhase => { @@ -32,6 +35,12 @@ export const frozenPhaseFromES = (phaseSerialized?: SerializedFrozenPhase): Froz phase.phaseEnabled = true; + if (phaseSerialized.actions.allocate) { + phase.dataTierAllocationType = determineDataTierAllocationType( + phaseSerialized.actions.allocate + ); + } + if (phaseSerialized.min_age) { const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); phase.selectedMinimumAge = minAge; @@ -80,19 +89,7 @@ export const frozenPhaseToES = ( esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; } - esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; - - if (phase.selectedNodeAttrs) { - const [name, value] = phase.selectedNodeAttrs.split(':'); - esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); - esPhase.actions.allocate.require = { - [name]: value, - }; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.require; - } - } + esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions); if (isNumber(phase.selectedReplicaCount)) { esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts new file mode 100644 index 0000000000000..0e7257d437ee7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts @@ -0,0 +1,464 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import cloneDeep from 'lodash/cloneDeep'; +import { serializePolicy } from './policy_serialization'; +import { + defaultNewColdPhase, + defaultNewDeletePhase, + defaultNewFrozenPhase, + defaultNewHotPhase, + defaultNewWarmPhase, +} from '../../constants'; +import { DataTierAllocationType } from '../../../../common/types'; + +describe('Policy serialization', () => { + test('serialize a policy using "default" data allocation', () => { + expect( + serializePolicy( + { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'default', + // These selected attrs should be ignored + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'default', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'default', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }, + { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + cold: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + frozen: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + }, + } + ) + ).toEqual({ + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + }, + warm: { + actions: { + set_priority: { + priority: 50, + }, + }, + }, + cold: { + actions: { + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + frozen: { + actions: { + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + }, + }); + }); + + test('serialize a policy using "custom" data allocation', () => { + expect( + serializePolicy( + { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: 'another:thing', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }, + { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { something: 'here' }, + }, + }, + }, + cold: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { something: 'here' }, + }, + }, + }, + frozen: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { something: 'here' }, + }, + }, + }, + }, + } + ) + ).toEqual({ + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + }, + warm: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { + another: 'thing', + }, + }, + set_priority: { + priority: 50, + }, + }, + }, + cold: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { + another: 'thing', + }, + }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + frozen: { + actions: { + allocate: { + include: { keep: 'this' }, + exclude: { keep: 'this' }, + require: { + another: 'thing', + }, + }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + }, + }); + }); + + test('serialize a policy using "custom" data allocation with no node attributes', () => { + expect( + serializePolicy( + { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: '', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: '', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'custom', + selectedNodeAttrs: '', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }, + { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + cold: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + frozen: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + }, + } + ) + ).toEqual({ + // There should be no allocation action in any phases... + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + }, + warm: { + actions: { + allocate: { include: {}, exclude: {}, require: { something: 'here' } }, + set_priority: { + priority: 50, + }, + }, + }, + cold: { + actions: { + allocate: { include: {}, exclude: {}, require: { something: 'here' } }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + frozen: { + actions: { + allocate: { include: {}, exclude: {}, require: { something: 'here' } }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + }, + }); + }); + + test('serialize a policy using "none" data allocation with no node attributes', () => { + expect( + serializePolicy( + { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'none', + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'none', + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'none', + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }, + { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + cold: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + frozen: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + }, + } + ) + ).toEqual({ + // There should be no allocation action in any phases... + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + }, + warm: { + actions: { + migrate: { + enabled: false, + }, + set_priority: { + priority: 50, + }, + }, + }, + cold: { + actions: { + migrate: { + enabled: false, + }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + frozen: { + actions: { + migrate: { + enabled: false, + }, + set_priority: { + priority: 0, + }, + }, + min_age: '0d', + }, + }, + }); + }); + + test('serialization does not alter the original policy', () => { + const originalPolicy = { + name: 'test', + phases: { + hot: { actions: {} }, + warm: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + cold: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + frozen: { + actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, + }, + }, + }; + + const originalClone = cloneDeep(originalPolicy); + + const deserializedPolicy = { + name: 'test', + phases: { + hot: { ...defaultNewHotPhase }, + warm: { + ...defaultNewWarmPhase, + dataTierAllocationType: 'none' as DataTierAllocationType, + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + cold: { + ...defaultNewColdPhase, + dataTierAllocationType: 'none' as DataTierAllocationType, + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + frozen: { + ...defaultNewFrozenPhase, + dataTierAllocationType: 'none' as DataTierAllocationType, + selectedNodeAttrs: 'ignore:this', + phaseEnabled: true, + }, + delete: { ...defaultNewDeletePhase }, + }, + }; + + serializePolicy(deserializedPolicy, originalPolicy); + deserializedPolicy.phases.warm.dataTierAllocationType = 'custom'; + serializePolicy(deserializedPolicy, originalPolicy); + deserializedPolicy.phases.warm.dataTierAllocationType = 'default'; + serializePolicy(deserializedPolicy, originalPolicy); + expect(originalPolicy).toEqual(originalClone); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts new file mode 100644 index 0000000000000..fe97b85778a53 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { serializePhaseWithAllocation } from './serialize_phase_with_allocation'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts new file mode 100644 index 0000000000000..5a9db3069aea6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import cloneDeep from 'lodash/cloneDeep'; + +import { + AllocateAction, + PhaseWithAllocationAction, + SerializedPhase, +} from '../../../../../common/types'; + +export const serializePhaseWithAllocation = ( + phase: PhaseWithAllocationAction, + originalPhaseActions: SerializedPhase['actions'] = {} +): SerializedPhase['actions'] => { + const esPhaseActions: SerializedPhase['actions'] = cloneDeep(originalPhaseActions); + + if (phase.dataTierAllocationType === 'custom' && phase.selectedNodeAttrs) { + const [name, value] = phase.selectedNodeAttrs.split(':'); + esPhaseActions.allocate = esPhaseActions.allocate || ({} as AllocateAction); + esPhaseActions.allocate.require = { + [name]: value, + }; + } else if (phase.dataTierAllocationType === 'none') { + esPhaseActions.migrate = { enabled: false }; + if (esPhaseActions.allocate) { + delete esPhaseActions.allocate; + } + } else if (phase.dataTierAllocationType === 'default') { + if (esPhaseActions.allocate) { + delete esPhaseActions.allocate.require; + } + delete esPhaseActions.migrate; + } + + return esPhaseActions; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts index cc815d67dbc18..6971f652f986b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts @@ -16,6 +16,9 @@ import { positiveNumbersAboveZeroErrorMessage, } from './policy_validation'; +import { determineDataTierAllocationType } from '../../lib'; +import { serializePhaseWithAllocation } from './shared'; + const warmPhaseInitialization: WarmPhase = { phaseEnabled: false, warmPhaseOnRollover: false, @@ -28,6 +31,7 @@ const warmPhaseInitialization: WarmPhase = { forceMergeEnabled: false, selectedForceMergeSegments: '', phaseIndexPriority: '', + dataTierAllocationType: 'default', }; export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhase => { @@ -39,6 +43,12 @@ export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhas phase.phaseEnabled = true; + if (phaseSerialized.actions.allocate) { + phase.dataTierAllocationType = determineDataTierAllocationType( + phaseSerialized.actions.allocate + ); + } + if (phaseSerialized.min_age) { if (phaseSerialized.min_age === '0ms') { phase.warmPhaseOnRollover = true; @@ -99,19 +109,7 @@ export const warmPhaseToES = ( delete esPhase.min_age; } - esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {}; - - if (phase.selectedNodeAttrs) { - const [name, value] = phase.selectedNodeAttrs.split(':'); - esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); - esPhase.actions.allocate.require = { - [name]: value, - }; - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.require; - } - } + esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions); if (isNumber(phase.selectedReplicaCount)) { esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 3075f9c89eb8d..84b8fa35cfe9b 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -82,10 +82,11 @@ export class IndexLifecycleManagementServerPlugin implements Plugin { - const attributes = nodeStats.attributes || {}; - for (const [key, value] of Object.entries(attributes)) { - const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key); - if (isNodeAttributeAllowed) { - const attributeString = `${key}:${value}`; - accum[attributeString] = accum[attributeString] || []; - accum[attributeString].push(nodeId); +interface Stats { + nodes: { + [nodeId: string]: { + attributes: Record; + roles: string[]; + }; + }; +} + +function convertStatsIntoList( + stats: Stats, + disallowedNodeAttributes: string[] +): ListNodesRouteResponse { + return Object.entries(stats.nodes).reduce( + (accum, [nodeId, nodeStats]) => { + const attributes = nodeStats.attributes || {}; + for (const [key, value] of Object.entries(attributes)) { + const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key); + if (isNodeAttributeAllowed) { + const attributeString = `${key}:${value}`; + accum.nodesByAttributes[attributeString] = accum.nodesByAttributes[attributeString] ?? []; + accum.nodesByAttributes[attributeString].push(nodeId); + } + } + + const dataRoles = nodeStats.roles.filter((r) => r.startsWith('data')) as NodeDataRole[]; + for (const role of dataRoles) { + accum.nodesByRoles[role as NodeDataRole] = accum.nodesByRoles[role] ?? []; + accum.nodesByRoles[role as NodeDataRole]!.push(nodeId); } - } - return accum; - }, {}); + return accum; + }, + { nodesByAttributes: {}, nodesByRoles: {} } as ListNodesRouteResponse + ); } async function fetchNodeStats(callAsCurrentUser: LegacyAPICaller): Promise { @@ -54,8 +77,8 @@ export function registerListRoute({ router, config, license, lib }: RouteDepende const stats = await fetchNodeStats( context.core.elasticsearch.legacy.client.callAsCurrentUser ); - const okResponse = { body: convertStatsIntoList(stats, disallowedNodeAttributes) }; - return response.ok(okResponse); + const body: ListNodesRouteResponse = convertStatsIntoList(stats, disallowedNodeAttributes); + return response.ok({ body }); } catch (e) { if (lib.isEsError(e)) { return response.customError({ diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index ba6b8665479a9..5ef38a0e46dc3 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -40,6 +40,8 @@ const setPrioritySchema = schema.maybe( const unfollowSchema = schema.maybe(schema.object({})); // Unfollow has no options +const migrateSchema = schema.maybe(schema.object({ enabled: schema.literal(false) })); + const allocateNodeSchema = schema.maybe(schema.recordOf(schema.string(), schema.string())); const allocateSchema = schema.maybe( schema.object({ @@ -76,6 +78,7 @@ const warmPhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, actions: schema.object({ + migrate: migrateSchema, set_priority: setPrioritySchema, unfollow: unfollowSchema, readonly: schema.maybe(schema.object({})), // Readonly has no options @@ -94,6 +97,7 @@ const coldPhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, actions: schema.object({ + migrate: migrateSchema, set_priority: setPrioritySchema, unfollow: unfollowSchema, allocate: allocateSchema, @@ -111,6 +115,7 @@ const frozenPhaseSchema = schema.maybe( schema.object({ min_age: minAgeSchema, actions: schema.object({ + migrate: migrateSchema, set_priority: setPrioritySchema, unfollow: unfollowSchema, allocate: allocateSchema, diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 4862b2a7e6a59..5cc976969d79c 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -52,6 +52,8 @@ const baseAlertRequestParamsRT = rt.intersection([ ]), criteria: rt.array(rt.any), alertInterval: rt.string, + alertThrottle: rt.string, + alertOnNoData: rt.boolean, }), ]); @@ -91,6 +93,7 @@ export const alertPreviewSuccessResponsePayloadRT = rt.type({ fired: rt.number, noData: rt.number, error: rt.number, + notifications: rt.number, }), }); export type AlertPreviewSuccessResponsePayload = rt.TypeOf< 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 877d047c941d4..02c3ea29c1846 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 @@ -33,6 +33,7 @@ import { getAlertPreview, PreviewableAlertTypes } from './get_alert_preview'; interface Props { alertInterval: string; + alertThrottle: string; alertType: PreviewableAlertTypes; fetch: HttpSetup['fetch']; alertParams: { criteria: any[]; sourceId: string } & Record; @@ -45,6 +46,7 @@ export const AlertPreview: React.FC = (props) => { const { alertParams, alertInterval, + alertThrottle, fetch, alertType, validate, @@ -73,16 +75,27 @@ export const AlertPreview: React.FC = (props) => { ...alertParams, lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M', alertInterval, + alertThrottle, + alertOnNoData: showNoDataResults ?? false, } as AlertPreviewRequestParams, alertType, }); - setPreviewResult({ ...result, groupByDisplayName, previewLookbackInterval }); + setPreviewResult({ ...result, groupByDisplayName, previewLookbackInterval, alertThrottle }); } catch (e) { setPreviewError(e); } finally { setIsPreviewLoading(false); } - }, [alertParams, alertInterval, fetch, alertType, groupByDisplayName, previewLookbackInterval]); + }, [ + alertParams, + alertInterval, + fetch, + alertType, + groupByDisplayName, + previewLookbackInterval, + alertThrottle, + showNoDataResults, + ]); const previewIntervalError = useMemo(() => { const intervalInSeconds = getIntervalInSeconds(alertInterval); @@ -101,6 +114,13 @@ export const AlertPreview: React.FC = (props) => { return hasValidationErrors || previewIntervalError; }, [alertParams.criteria, previewIntervalError, validate]); + const showNumberOfNotifications = useMemo(() => { + if (!previewResult) return false; + const { notifications, fired, noData, error } = previewResult.resultTotals; + const unthrottledNotifications = fired + (showNoDataResults ? noData + error : 0); + return unthrottledNotifications > notifications; + }, [previewResult, showNoDataResults]); + return ( = (props) => { <> - {previewResult.resultTotals.fired}{' '} - {previewResult.resultTotals.fired === 1 - ? firedTimeLabel - : firedTimesLabel} + ), }} @@ -173,7 +196,7 @@ export const AlertPreview: React.FC = (props) => { ) : null} e.value === previewResult.previewLookbackInterval @@ -211,6 +234,32 @@ export const AlertPreview: React.FC = (props) => { defaultMessage="An error occurred when trying to evaluate some of the data." /> ) : null} + {showNumberOfNotifications ? ( + <> + + + {i18n.translate( + 'xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber', + { + defaultMessage: + '{notifs, plural, one {# notification} other {# notifications}}', + values: { + notifs: previewResult.resultTotals.notifications, + }, + } + )} + + ), + }} + /> + + ) : null}{' '} )} @@ -218,6 +267,7 @@ export const AlertPreview: React.FC = (props) => { <> = (props) => { {previewError.body?.statusCode === 508 ? ( = (props) => { ) : ( = previewOptions.map((o) => omit(o, 'shortText') ); - -const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', { - defaultMessage: 'time', -}); -const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', { - defaultMessage: 'times', -}); diff --git a/x-pack/plugins/infra/public/alerting/common/index.ts b/x-pack/plugins/infra/public/alerting/common/index.ts index e1b4a70cfb1fc..384391578f0c6 100644 --- a/x-pack/plugins/infra/public/alerting/common/index.ts +++ b/x-pack/plugins/infra/public/alerting/common/index.ts @@ -45,10 +45,3 @@ export const previewOptions = [ }), }, ]; - -export const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', { - defaultMessage: 'time', -}); -export const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', { - defaultMessage: 'times', -}); diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx index ada7a30a859e0..60a00371e5ade 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx @@ -69,6 +69,7 @@ describe('Expression', () => { Reflect.set(alertParams, key, value)} 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 5ac2f407839e4..f47f30c280b2a 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -89,6 +89,7 @@ interface Props { alertOnNoData?: boolean; }; alertInterval: string; + alertThrottle: string; alertsContext: AlertsContextValue; setAlertParams(key: string, value: any): void; setAlertProperty(key: string, value: any): void; @@ -104,7 +105,14 @@ const defaultExpression = { } as InventoryMetricConditions; export const Expressions: React.FC = (props) => { - const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props; + const { + setAlertParams, + alertParams, + errors, + alertsContext, + alertInterval, + alertThrottle, + } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', type: 'metrics', @@ -378,6 +386,7 @@ export const Expressions: React.FC = (props) => { { Reflect.set(alertParams, key, value)} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 6b102045fa516..c71a3b6b13338 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -51,6 +51,7 @@ interface Props { alertParams: AlertParams; alertsContext: AlertsContextValue; alertInterval: string; + alertThrottle: string; setAlertParams(key: string, value: any): void; setAlertProperty(key: string, value: any): void; } @@ -65,7 +66,14 @@ const defaultExpression = { export { defaultExpression }; export const Expressions: React.FC = (props) => { - const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props; + const { + setAlertParams, + alertParams, + errors, + alertsContext, + alertInterval, + alertThrottle, + } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', type: 'metrics', @@ -399,6 +407,7 @@ export const Expressions: React.FC = (props) => { { const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams; @@ -52,6 +56,10 @@ export const previewInventoryMetricThresholdAlert = async ({ const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; + const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle); + const executionsPerThrottle = Math.floor( + (throttleIntervalInSeconds / alertIntervalInSeconds) * alertResultsPerExecution + ); try { const results = await Promise.all( criteria.map((c) => @@ -66,6 +74,12 @@ export const previewInventoryMetricThresholdAlert = async ({ let numberOfTimesFired = 0; let numberOfNoDataResults = 0; let numberOfErrors = 0; + let numberOfNotifications = 0; + let throttleTracker = 0; + const notifyWithThrottle = () => { + if (throttleTracker === 0) numberOfNotifications++; + throttleTracker++; + }; for (let i = 0; i < numberOfExecutionBuckets; i++) { const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); const allConditionsFiredInMappedBucket = results.every((result) => { @@ -79,11 +93,27 @@ export const previewInventoryMetricThresholdAlert = async ({ const someConditionsErrorInMappedBucket = results.some((result) => { return result[item].isError; }); - if (allConditionsFiredInMappedBucket) numberOfTimesFired++; - if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; - if (someConditionsErrorInMappedBucket) numberOfErrors++; + if (someConditionsErrorInMappedBucket) { + numberOfErrors++; + if (alertOnNoData) { + notifyWithThrottle(); + } + } else if (someConditionsNoDataInMappedBucket) { + numberOfNoDataResults++; + if (alertOnNoData) { + notifyWithThrottle(); + } + } else if (allConditionsFiredInMappedBucket) { + numberOfTimesFired++; + notifyWithThrottle(); + } else if (throttleTracker > 0) { + throttleTracker++; + } + if (throttleTracker === executionsPerThrottle) { + throttleTracker = 0; + } } - return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications]; }); return previewResults; 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 c26b44dfe8ff8..73e17537476c8 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 @@ -16,11 +16,14 @@ describe('Previewing the metric threshold alert type', () => { ...baseParams, lookback: 'h', alertInterval: '1m', + alertThrottle: '1m', + alertOnNoData: true, }); - const [firedResults, noDataResults, errorResults] = ungroupedResult; + const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; expect(firedResults).toBe(30); expect(noDataResults).toBe(0); expect(errorResults).toBe(0); + expect(notifications).toBe(30); }); test('returns the expected results using a bucket interval shorter than the alert interval', async () => { @@ -28,22 +31,42 @@ describe('Previewing the metric threshold alert type', () => { ...baseParams, lookback: 'h', alertInterval: '3m', + alertThrottle: '3m', + alertOnNoData: true, }); - const [firedResults, noDataResults, errorResults] = ungroupedResult; + const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; expect(firedResults).toBe(10); expect(noDataResults).toBe(0); expect(errorResults).toBe(0); + expect(notifications).toBe(10); }); test('returns the expected results using a bucket interval longer than the alert interval', async () => { const [ungroupedResult] = await previewMetricThresholdAlert({ ...baseParams, lookback: 'h', alertInterval: '30s', + alertThrottle: '30s', + alertOnNoData: true, }); - const [firedResults, noDataResults, errorResults] = ungroupedResult; + const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; expect(firedResults).toBe(60); expect(noDataResults).toBe(0); expect(errorResults).toBe(0); + expect(notifications).toBe(60); + }); + test('returns the expected results using a throttle interval longer than the alert interval', async () => { + const [ungroupedResult] = await previewMetricThresholdAlert({ + ...baseParams, + lookback: 'h', + alertInterval: '1m', + alertThrottle: '3m', + alertOnNoData: true, + }); + const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; + expect(firedResults).toBe(30); + expect(noDataResults).toBe(0); + expect(errorResults).toBe(0); + expect(notifications).toBe(15); }); }); describe('querying with a groupBy parameter', () => { @@ -56,15 +79,19 @@ describe('Previewing the metric threshold alert type', () => { }, lookback: 'h', alertInterval: '1m', + alertThrottle: '1m', + alertOnNoData: true, }); - const [firedResultsA, noDataResultsA, errorResultsA] = resultA; + const [firedResultsA, noDataResultsA, errorResultsA, notificationsA] = resultA; expect(firedResultsA).toBe(30); expect(noDataResultsA).toBe(0); expect(errorResultsA).toBe(0); - const [firedResultsB, noDataResultsB, errorResultsB] = resultB; + expect(notificationsA).toBe(30); + const [firedResultsB, noDataResultsB, errorResultsB, notificationsB] = resultB; expect(firedResultsB).toBe(60); expect(noDataResultsB).toBe(0); expect(errorResultsB).toBe(0); + expect(notificationsB).toBe(60); }); }); describe('querying a data set with a period of No Data', () => { @@ -82,11 +109,14 @@ describe('Previewing the metric threshold alert type', () => { }, lookback: 'h', alertInterval: '1m', + alertThrottle: '1m', + alertOnNoData: true, }); - const [firedResults, noDataResults, errorResults] = ungroupedResult; + const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult; expect(firedResults).toBe(25); expect(noDataResults).toBe(10); expect(errorResults).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 0f2afda663da8..e1615625d605a 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 @@ -28,6 +28,8 @@ interface PreviewMetricThresholdAlertParams { config: InfraSource['configuration']; lookback: Unit; alertInterval: string; + alertThrottle: string; + alertOnNoData: boolean; end?: number; overrideLookbackIntervalInSeconds?: number; } @@ -43,6 +45,8 @@ export const previewMetricThresholdAlert: ( config, lookback, alertInterval, + alertThrottle, + alertOnNoData, end = Date.now(), overrideLookbackIntervalInSeconds, }, @@ -77,6 +81,11 @@ export const previewMetricThresholdAlert: ( // Now determine how to interpolate this histogram based on the alert interval const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; + const throttleIntervalInSeconds = Math.max( + getIntervalInSeconds(alertThrottle), + alertIntervalInSeconds + ); + const previewResults = await Promise.all( groups.map(async (group) => { // Interpolate the buckets returned by evaluateAlert and return a count of how many of these @@ -90,6 +99,12 @@ export const previewMetricThresholdAlert: ( let numberOfTimesFired = 0; let numberOfNoDataResults = 0; let numberOfErrors = 0; + let numberOfNotifications = 0; + let throttleTracker = 0; + const notifyWithThrottle = () => { + if (throttleTracker === 0) numberOfNotifications++; + throttleTracker += alertIntervalInSeconds; + }; for (let i = 0; i < numberOfExecutionBuckets; i++) { const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); const allConditionsFiredInMappedBucket = alertResults.every( @@ -102,11 +117,27 @@ export const previewMetricThresholdAlert: ( const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => { return alertResult[group].isError; }); - if (allConditionsFiredInMappedBucket) numberOfTimesFired++; - if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++; - if (someConditionsErrorInMappedBucket) numberOfErrors++; + if (someConditionsErrorInMappedBucket) { + numberOfErrors++; + if (alertOnNoData) { + notifyWithThrottle(); + } + } else if (someConditionsNoDataInMappedBucket) { + numberOfNoDataResults++; + if (alertOnNoData) { + notifyWithThrottle(); + } + } else if (allConditionsFiredInMappedBucket) { + numberOfTimesFired++; + notifyWithThrottle(); + } else if (throttleTracker > 0) { + throttleTracker += alertIntervalInSeconds; + } + if (throttleTracker >= throttleIntervalInSeconds) { + throttleTracker = 0; + } } - return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors]; + return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications]; }) ); return previewResults; @@ -114,7 +145,15 @@ export const previewMetricThresholdAlert: ( if (isTooManyBucketsPreviewException(e)) { // If there's too much data on the first request, recursively slice the lookback interval // until all the data can be retrieved - const basePreviewParams = { callCluster, params, config, lookback, alertInterval }; + const basePreviewParams = { + callCluster, + params, + config, + lookback, + alertInterval, + alertThrottle, + alertOnNoData, + }; const { maxBuckets } = e; // If this is still the first iteration, try to get the number of groups in order to // calculate max buckets. If this fails, just estimate based on 1 group @@ -159,7 +198,7 @@ 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]]; + return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]]; }) ); return zippedResult; diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 40d09dadfe050..1233e9d2d1357 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -30,7 +30,16 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) }, }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { - const { criteria, filterQuery, lookback, sourceId, alertType, alertInterval } = request.body; + const { + criteria, + filterQuery, + lookback, + sourceId, + alertType, + alertInterval, + alertThrottle, + alertOnNoData, + } = request.body; const callCluster = (endpoint: string, opts: Record) => { return callWithRequest(requestContext, endpoint, opts); @@ -51,22 +60,26 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) lookback, config: source.configuration, alertInterval, + alertThrottle, + alertOnNoData, }); const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, [firedResult, noDataResult, errorResult]) => { + (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, } ); return response.ok({ @@ -84,22 +97,26 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) lookback, source, alertInterval, + alertThrottle, + alertOnNoData, }); const numberOfGroups = previewResult.length; const resultTotals = previewResult.reduce( - (totals, [firedResult, noDataResult, errorResult]) => { + (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, } ); diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 47900415466b9..f0f7bca29c99e 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -16,6 +16,7 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { LicensingPluginSetup, ILicense } from '../../licensing/server'; import { EncryptedSavedObjectsPluginStart, @@ -181,6 +182,7 @@ export class IngestManagerPlugin id: PLUGIN_ID, name: 'Ingest Manager', icon: 'savedObjectsApp', + category: DEFAULT_APP_CATEGORIES.management, navLinkId: PLUGIN_ID, app: [PLUGIN_ID, 'kibana'], catalogue: ['ingestManager'], diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts index 83ad08d09de76..dfa03ec9d527d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { saveInstalledEsRefs } from '../../packages/install'; import * as Registry from '../../registry'; @@ -33,100 +33,92 @@ export const installTransformForDataset = async ( registryPackage: RegistryPackage, paths: string[], callCluster: CallESAsCurrentUser, - savedObjectsClient: SavedObjectsClientContract, - logger: Logger + savedObjectsClient: SavedObjectsClientContract ) => { - try { - const installation = await getInstallation({ - savedObjectsClient, - pkgName: registryPackage.name, - }); - let previousInstalledTransformEsAssets: EsAssetReference[] = []; - if (installation) { - previousInstalledTransformEsAssets = installation.installed_es.filter( - ({ type, id }) => type === ElasticsearchAssetType.transform - ); - } - - // delete all previous transform - await deleteTransforms( - callCluster, - previousInstalledTransformEsAssets.map((asset) => asset.id) + const installation = await getInstallation({ + savedObjectsClient, + pkgName: registryPackage.name, + }); + let previousInstalledTransformEsAssets: EsAssetReference[] = []; + if (installation) { + previousInstalledTransformEsAssets = installation.installed_es.filter( + ({ type, id }) => type === ElasticsearchAssetType.transform ); - // install the latest dataset - const datasets = registryPackage.datasets; - if (!datasets?.length) return []; - const installNameSuffix = `${registryPackage.version}`; - - const transformPaths = paths.filter((path) => isTransform(path)); - let installedTransforms: EsAssetReference[] = []; - if (transformPaths.length > 0) { - const transformPathDatasets = datasets.reduce((acc, dataset) => { - transformPaths.forEach((path) => { - if (isDatasetTransform(path, dataset.path)) { - acc.push({ path, dataset }); - } - }); - return acc; - }, []); - - const transformRefs = transformPathDatasets.reduce( - (acc, transformPathDataset) => { - if (transformPathDataset) { - acc.push({ - id: getTransformNameForInstallation(transformPathDataset, installNameSuffix), - type: ElasticsearchAssetType.transform, - }); - } - return acc; - }, - [] - ); - - // get and save transform refs before installing transforms - await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs); - - const transforms: TransformInstallation[] = transformPathDatasets.map( - (transformPathDataset: TransformPathDataset) => { - return { - installationName: getTransformNameForInstallation( - transformPathDataset, - installNameSuffix - ), - content: getAsset(transformPathDataset.path).toString('utf-8'), - }; - } - ); + } - const installationPromises = transforms.map(async (transform) => { - return installTransform({ callCluster, transform, logger }); + // delete all previous transform + await deleteTransforms( + callCluster, + previousInstalledTransformEsAssets.map((asset) => asset.id) + ); + // install the latest dataset + const datasets = registryPackage.datasets; + if (!datasets?.length) return []; + const installNameSuffix = `${registryPackage.version}`; + + const transformPaths = paths.filter((path) => isTransform(path)); + let installedTransforms: EsAssetReference[] = []; + if (transformPaths.length > 0) { + const transformPathDatasets = datasets.reduce((acc, dataset) => { + transformPaths.forEach((path) => { + if (isDatasetTransform(path, dataset.path)) { + acc.push({ path, dataset }); + } }); + return acc; + }, []); + + const transformRefs = transformPathDatasets.reduce( + (acc, transformPathDataset) => { + if (transformPathDataset) { + acc.push({ + id: getTransformNameForInstallation(transformPathDataset, installNameSuffix), + type: ElasticsearchAssetType.transform, + }); + } + return acc; + }, + [] + ); - installedTransforms = await Promise.all(installationPromises).then((results) => - results.flat() - ); - } + // get and save transform refs before installing transforms + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs); + + const transforms: TransformInstallation[] = transformPathDatasets.map( + (transformPathDataset: TransformPathDataset) => { + return { + installationName: getTransformNameForInstallation( + transformPathDataset, + installNameSuffix + ), + content: getAsset(transformPathDataset.path).toString('utf-8'), + }; + } + ); - if (previousInstalledTransformEsAssets.length > 0) { - const currentInstallation = await getInstallation({ - savedObjectsClient, - pkgName: registryPackage.name, - }); + const installationPromises = transforms.map(async (transform) => { + return installTransform({ callCluster, transform }); + }); - // remove the saved object reference - await deleteTransformRefs( - savedObjectsClient, - currentInstallation?.installed_es || [], - registryPackage.name, - previousInstalledTransformEsAssets.map((asset) => asset.id), - installedTransforms.map((installed) => installed.id) - ); - } - return installedTransforms; - } catch (err) { - logger.error(err); - throw err; + installedTransforms = await Promise.all(installationPromises).then((results) => results.flat()); } + + if (previousInstalledTransformEsAssets.length > 0) { + const currentInstallation = await getInstallation({ + savedObjectsClient, + pkgName: registryPackage.name, + }); + + // remove the saved object reference + await deleteTransformRefs( + savedObjectsClient, + currentInstallation?.installed_es || [], + registryPackage.name, + previousInstalledTransformEsAssets.map((asset) => asset.id), + installedTransforms.map((installed) => installed.id) + ); + } + return installedTransforms; }; const isTransform = (path: string) => { @@ -147,31 +139,24 @@ const isDatasetTransform = (path: string, datasetName: string) => { async function installTransform({ callCluster, transform, - logger, }: { callCluster: CallESAsCurrentUser; transform: TransformInstallation; - logger: Logger; }): Promise { - try { - // defer validation on put if the source index is not available - await callCluster('transport.request', { - method: 'PUT', - path: `/_transform/${transform.installationName}`, - query: 'defer_validation=true', - body: transform.content, - }); - - await callCluster('transport.request', { - method: 'POST', - path: `/_transform/${transform.installationName}/_start`, - }); - - return { id: transform.installationName, type: ElasticsearchAssetType.transform }; - } catch (err) { - logger.error(err); - throw err; - } + // defer validation on put if the source index is not available + await callCluster('transport.request', { + method: 'PUT', + path: `/_transform/${transform.installationName}`, + query: 'defer_validation=true', + body: transform.content, + }); + + await callCluster('transport.request', { + method: 'POST', + path: `/_transform/${transform.installationName}/_start`, + }); + + return { id: transform.installationName, type: ElasticsearchAssetType.transform }; } const getTransformNameForInstallation = ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts index bb506ecad0ade..c43a33df2db61 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loggingSystemMock } from '../../../../../../../../src/core/server/logging/logging_system.mock'; - jest.mock('../../packages/get', () => { return { getInstallation: jest.fn(), getInstallationObject: jest.fn() }; }); @@ -18,12 +15,7 @@ jest.mock('./common', () => { }); import { installTransformForDataset } from './install'; -import { - ILegacyScopedClusterClient, - LoggerFactory, - SavedObject, - SavedObjectsClientContract, -} from 'kibana/server'; +import { ILegacyScopedClusterClient, SavedObject, SavedObjectsClientContract } from 'kibana/server'; import { ElasticsearchAssetType, Installation, RegistryPackage } from '../../../../types'; import { getInstallation, getInstallationObject } from '../../packages'; import { getAsset } from './common'; @@ -33,7 +25,6 @@ import { savedObjectsClientMock } from '../../../../../../../../src/core/server/ describe('test transform install', () => { let legacyScopedClusterClient: jest.Mocked; let savedObjectsClient: jest.Mocked; - let logger: jest.Mocked; beforeEach(() => { legacyScopedClusterClient = { callAsInternalUser: jest.fn(), @@ -42,7 +33,6 @@ describe('test transform install', () => { (getInstallation as jest.MockedFunction).mockReset(); (getInstallationObject as jest.MockedFunction).mockReset(); savedObjectsClient = savedObjectsClientMock.create(); - logger = loggingSystemMock.create(); }); afterEach(() => { @@ -142,8 +132,7 @@ describe('test transform install', () => { 'endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json', ], legacyScopedClusterClient.callAsCurrentUser, - savedObjectsClient, - logger.get('ingest') + savedObjectsClient ); expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([ [ @@ -297,8 +286,7 @@ describe('test transform install', () => { } as unknown) as RegistryPackage, ['endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json'], legacyScopedClusterClient.callAsCurrentUser, - savedObjectsClient, - logger.get('ingest') + savedObjectsClient ); expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([ @@ -395,8 +383,7 @@ describe('test transform install', () => { } as unknown) as RegistryPackage, [], legacyScopedClusterClient.callAsCurrentUser, - savedObjectsClient, - logger.get('ingest') + savedObjectsClient ); expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([ diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 4179e82d6ad1d..54b9c4d3fbb17 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -36,7 +36,6 @@ import { deleteKibanaSavedObjectsAssets } from './remove'; import { PackageOutdatedError } from '../../../errors'; import { getPackageSavedObjects } from './get'; import { installTransformForDataset } from '../elasticsearch/transform/install'; -import { appContextService } from '../../app_context'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -197,8 +196,7 @@ export async function installPackage({ registryPackageInfo, paths, callCluster, - savedObjectsClient, - appContextService.getLogger() + savedObjectsClient ); // if this is an update or retrying an update, delete the previous version's pipelines diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx index 91adbcecaf897..077e07a89f788 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -118,6 +118,7 @@ export const QueryInput = ({ return ( void }) => ( - + {label} ); diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index 365e430a460fa..50b8f4c6fc40b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -126,6 +126,7 @@ export function PieToolbar(props: VisualizationToolbarProps & { * Adjusts the borders for groupings */ groupPosition?: 'none' | 'left' | 'center' | 'right'; + dataTestSubj?: string; }; export const ToolbarButton: React.FunctionComponent = ({ @@ -42,6 +43,7 @@ export const ToolbarButton: React.FunctionComponent = ({ size = 'm', hasArrow = true, groupPosition = 'none', + dataTestSubj = '', ...rest }) => { const classes = classNames( @@ -52,6 +54,7 @@ export const ToolbarButton: React.FunctionComponent = ({ ); return ( = ({ @@ -39,6 +40,7 @@ export const ToolbarPopover: React.FunctionComponent = ({ type, isDisabled = false, groupPosition, + buttonDataTestSubj, }) => { const [open, setOpen] = useState(false); @@ -60,6 +62,7 @@ export const ToolbarPopover: React.FunctionComponent = ({ hasArrow={false} isDisabled={isDisabled} groupPosition={groupPosition} + dataTestSubj={buttonDataTestSubj} > diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx index 835f3e2cde769..45ec7098aa639 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx @@ -73,7 +73,12 @@ export interface AxisSettingsPopoverProps { const popoverConfig = ( axis: AxesSettingsConfigKeys, isHorizontal: boolean -): { icon: IconType; groupPosition: ToolbarButtonProps['groupPosition']; popoverTitle: string } => { +): { + icon: IconType; + groupPosition: ToolbarButtonProps['groupPosition']; + popoverTitle: string; + buttonDataTestSubj: string; +} => { switch (axis) { case 'yLeft': return { @@ -86,6 +91,7 @@ const popoverConfig = ( : i18n.translate('xpack.lens.xyChart.leftAxisLabel', { defaultMessage: 'Left axis', }), + buttonDataTestSubj: 'lnsLeftAxisButton', }; case 'yRight': return { @@ -98,6 +104,7 @@ const popoverConfig = ( : i18n.translate('xpack.lens.xyChart.rightAxisLabel', { defaultMessage: 'Right axis', }), + buttonDataTestSubj: 'lnsRightAxisButton', }; case 'x': default: @@ -111,6 +118,8 @@ const popoverConfig = ( : i18n.translate('xpack.lens.xyChart.bottomAxisLabel', { defaultMessage: 'Bottom axis', }), + + buttonDataTestSubj: 'lnsBottomAxisButton', }; } }; @@ -143,6 +152,7 @@ export const AxisSettingsPopover: React.FunctionComponent diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 4aa5bd62c05a5..c7781c2e1d50c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -208,6 +208,7 @@ export function XyToolbar(props: VisualizationToolbarProps) { isDisabled={!hasNonBarSeries} type="values" groupPosition="left" + buttonDataTestSubj="lnsMissingValuesButton" > ) { })} > { return { @@ -488,6 +490,7 @@ const ColorPicker = ({ const colorPicker = ( { expect(first.type).toBe('basic'); trigger$.next(); + // waiting on a promise gives the exhaustMap time to complete and not de-dupe these calls + await Promise.resolve(); trigger$.next(); const [, second] = await license$.pipe(take(2), toArray()).toPromise(); @@ -89,18 +91,15 @@ describe('licensing update', () => { expect(fetcher).toHaveBeenCalledTimes(1); }); - it('handles fetcher race condition', async () => { + it('ignores trigger if license fetching is delayed ', async () => { const delayMs = 100; - let firstCall = true; - const fetcher = jest.fn().mockImplementation( + const fetcher = jest.fn().mockImplementationOnce( () => new Promise((resolve) => { - if (firstCall) { - firstCall = false; - setTimeout(() => resolve(licenseMock.createLicense()), delayMs); - } else { - resolve(licenseMock.createLicense({ license: { type: 'gold' } })); - } + setTimeout( + () => resolve(licenseMock.createLicense({ license: { type: 'gold' } })), + delayMs + ); }) ); const trigger$ = new Subject(); @@ -113,7 +112,7 @@ describe('licensing update', () => { await delay(delayMs * 2); - await expect(fetcher).toHaveBeenCalledTimes(2); + await expect(fetcher).toHaveBeenCalledTimes(1); await expect(values).toHaveLength(1); await expect(values[0].type).toBe('gold'); }); @@ -144,7 +143,7 @@ describe('licensing update', () => { expect(fetcher).toHaveBeenCalledTimes(0); }); - it('refreshManually guarantees license fetching', async () => { + it(`refreshManually multiple times gets new license`, async () => { const trigger$ = new Subject(); const firstLicense = licenseMock.createLicense({ license: { uid: 'first', type: 'basic' } }); const secondLicense = licenseMock.createLicense({ license: { uid: 'second', type: 'gold' } }); diff --git a/x-pack/plugins/licensing/common/license_update.ts b/x-pack/plugins/licensing/common/license_update.ts index 0197ca5396ad1..cd5052b0b49a3 100644 --- a/x-pack/plugins/licensing/common/license_update.ts +++ b/x-pack/plugins/licensing/common/license_update.ts @@ -5,32 +5,41 @@ */ import { ConnectableObservable, Observable, Subject, from, merge } from 'rxjs'; -import { filter, map, pairwise, switchMap, publishReplay, takeUntil } from 'rxjs/operators'; +import { + filter, + map, + pairwise, + exhaustMap, + publishReplay, + share, + take, + takeUntil, +} from 'rxjs/operators'; import { hasLicenseInfoChanged } from './has_license_info_changed'; import { ILicense } from './types'; export function createLicenseUpdate( - trigger$: Observable, + triggerRefresh$: Observable, stop$: Observable, fetcher: () => Promise, initialValues?: ILicense ) { - const triggerRefresh$ = trigger$.pipe(switchMap(fetcher)); - const manuallyFetched$ = new Subject(); + const manuallyRefresh$ = new Subject(); + const fetched$ = merge(triggerRefresh$, manuallyRefresh$).pipe(exhaustMap(fetcher), share()); - const fetched$ = merge(triggerRefresh$, manuallyFetched$).pipe( + const cached$ = fetched$.pipe( takeUntil(stop$), publishReplay(1) // have to cast manually as pipe operator cannot return ConnectableObservable // https://github.com/ReactiveX/rxjs/issues/2972 ) as ConnectableObservable; - const fetchSubscription = fetched$.connect(); - stop$.subscribe({ complete: () => fetchSubscription.unsubscribe() }); + const cachedSubscription = cached$.connect(); + stop$.subscribe({ complete: () => cachedSubscription.unsubscribe() }); const initialValues$ = initialValues ? from([undefined, initialValues]) : from([undefined]); - const license$: Observable = merge(initialValues$, fetched$).pipe( + const license$: Observable = merge(initialValues$, cached$).pipe( pairwise(), filter(([previous, next]) => hasLicenseInfoChanged(previous, next!)), map(([, next]) => next!) @@ -38,10 +47,10 @@ export function createLicenseUpdate( return { license$, - async refreshManually() { - const license = await fetcher(); - manuallyFetched$.next(license); - return license; + refreshManually() { + const licensePromise = fetched$.pipe(take(1)).toPromise(); + manuallyRefresh$.next(); + return licensePromise; }, }; } diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts index 960fe3699e210..c20563dd15913 100644 --- a/x-pack/plugins/licensing/public/plugin.test.ts +++ b/x-pack/plugins/licensing/public/plugin.test.ts @@ -115,7 +115,9 @@ describe('licensing plugin', () => { refresh(); } else if (i === 2) { expect(value.type).toBe('gold'); - refresh(); + // since this is a synchronous subscription, we need to give the exhaustMap a chance + // to mark the subscription as complete before emitting another value on the Subject + process.nextTick(() => refresh()); } else if (i === 3) { expect(value.type).toBe('platinum'); done(); diff --git a/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts b/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts index 5c768a00783a8..af3ec42ab4ec5 100644 --- a/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts +++ b/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts @@ -25,7 +25,23 @@ describe('createOnPreResponseHandler', () => { }, }); }); - it('sets license.signature header after refresh for non-error responses', async () => { + it('sets license.signature header immediately for 429 error responses', async () => { + const refresh = jest.fn(); + const license$ = new BehaviorSubject(licenseMock.createLicense({ signature: 'foo' })); + const toolkit = httpServiceMock.createOnPreResponseToolkit(); + + const interceptor = createOnPreResponseHandler(refresh, license$); + await interceptor(httpServerMock.createKibanaRequest(), { statusCode: 429 }, toolkit); + + expect(refresh).toHaveBeenCalledTimes(0); + expect(toolkit.next).toHaveBeenCalledTimes(1); + expect(toolkit.next).toHaveBeenCalledWith({ + headers: { + 'kbn-license-sig': 'foo', + }, + }); + }); + it('sets license.signature header after refresh for other error responses', async () => { const updatedLicense = licenseMock.createLicense({ signature: 'bar' }); const license$ = new BehaviorSubject(licenseMock.createLicense({ signature: 'foo' })); const refresh = jest.fn().mockImplementation( diff --git a/x-pack/plugins/licensing/server/on_pre_response_handler.ts b/x-pack/plugins/licensing/server/on_pre_response_handler.ts index c8befceb4fe32..6428e41b18058 100644 --- a/x-pack/plugins/licensing/server/on_pre_response_handler.ts +++ b/x-pack/plugins/licensing/server/on_pre_response_handler.ts @@ -15,9 +15,11 @@ export function createOnPreResponseHandler( return async (req, res, t) => { // If we're returning an error response, refresh license info from // Elasticsearch in case the error is due to a change in license information - // in Elasticsearch. - // https://github.com/elastic/x-pack-kibana/pull/2876 - if (res.statusCode >= 400) { + // in Elasticsearch. https://github.com/elastic/x-pack-kibana/pull/2876 + // We're explicit ignoring a 429 "Too Many Requests". This is being used to communicate + // that back-pressure should be applied, and we don't need to refresh the license in these + // situations. + if (res.statusCode >= 400 && res.statusCode !== 429) { await refresh(); } const license = await license$.pipe(take(1)).toPromise(); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 2e0ba7cf3efee..f565321f87ef7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -15,7 +15,7 @@ import { RENDER_AS, SOURCE_TYPES, } from '../../../../common/constants'; -import { SearchSource } from '../../../../../../../src/plugins/data/public/search/search_source'; +import { SearchSource } from 'src/plugins/data/public'; export class MockSearchSource { setField = jest.fn(); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts index 3223d0c94178f..0bc9bba7816ca 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts @@ -9,7 +9,7 @@ jest.mock('../../../kibana_services'); jest.mock('./load_index_settings'); import { getIndexPatternService, getSearchService, getHttp } from '../../../kibana_services'; -import { SearchSource } from '../../../../../../../src/plugins/data/public/search/search_source'; +import { SearchSource } from 'src/plugins/data/public'; // @ts-expect-error import { loadIndexSettings } from './load_index_settings'; diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 5eb0482905e36..46e39fcdac27a 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -6,6 +6,7 @@ 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 import { getEcommerceSavedObjects } from './sample_data/ecommerce_saved_objects'; @@ -168,7 +169,8 @@ export class MapsPlugin implements Plugin { name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', { defaultMessage: 'Maps', }), - order: 600, + order: 400, + category: DEFAULT_APP_CATEGORIES.kibana, icon: APP_ICON, navLinkId: APP_ID, app: [APP_ID, 'kibana'], diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index 791a7de48f36f..9a415ac0718b3 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -5,3 +5,5 @@ */ export { SearchResponse7 } from './types/es_client'; +export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './constants/anomalies'; +export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; diff --git a/x-pack/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/plugins/ml/common/types/file_datavisualizer.ts index a8b775c8d5f60..9dc3896e9be48 100644 --- a/x-pack/plugins/ml/common/types/file_datavisualizer.ts +++ b/x-pack/plugins/ml/common/types/file_datavisualizer.ts @@ -29,6 +29,8 @@ export interface FindFileStructureResponse { count: number; cardinality: number; top_hits: Array<{ count: number; value: any }>; + max_value?: number; + min_value?: number; }; }; sample_start: string; @@ -42,7 +44,7 @@ export interface FindFileStructureResponse { delimiter: string; need_client_timezone: boolean; num_lines_analyzed: number; - column_names: string[]; + column_names?: string[]; explanation?: string[]; grok_pattern?: string; multiline_start_pattern?: string; diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 2c5dbe108ab1e..1cd52079b4e39 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -35,5 +35,8 @@ "dashboard", "savedObjects", "home" + ], + "extraPublicDirs": [ + "common" ] } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_field_label.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_field_label.tsx new file mode 100644 index 0000000000000..610b29c85a062 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_field_label.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; + +import { CombinedField } from './types'; + +export function CombinedFieldLabel({ combinedField }: { combinedField: CombinedField }) { + return {getCombinedFieldLabel(combinedField)}; +} + +function getCombinedFieldLabel(combinedField: CombinedField) { + return `${combinedField.fieldNames.join(combinedField.delimiter)} => ${ + combinedField.combinedFieldName + } (${combinedField.mappingType})`; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_form.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_form.tsx new file mode 100644 index 0000000000000..fdfe10c2acf02 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_form.tsx @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Component } from 'react'; + +import { + EuiFormRow, + EuiPopover, + EuiContextMenu, + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + +import { CombinedField } from './types'; +import { GeoPointForm } from './geo_point'; +import { CombinedFieldLabel } from './combined_field_label'; +import { + addCombinedFieldsToMappings, + addCombinedFieldsToPipeline, + getNameCollisionMsg, + removeCombinedFieldsFromMappings, + removeCombinedFieldsFromPipeline, +} from './utils'; +import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; + +interface Props { + mappingsString: string; + pipelineString: string; + onMappingsStringChange(): void; + onPipelineStringChange(): void; + combinedFields: CombinedField[]; + onCombinedFieldsChange(combinedFields: CombinedField[]): void; + results: FindFileStructureResponse; + isDisabled: boolean; +} + +interface State { + isPopoverOpen: boolean; +} + +export class CombinedFieldsForm extends Component { + state: State = { + isPopoverOpen: false, + }; + + togglePopover = () => { + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + addCombinedField = (combinedField: CombinedField) => { + if (this.hasNameCollision(combinedField.combinedFieldName)) { + throw new Error(getNameCollisionMsg(combinedField.combinedFieldName)); + } + + const mappings = this.parseMappings(); + const pipeline = this.parsePipeline(); + + this.props.onMappingsStringChange( + // @ts-expect-error + JSON.stringify(addCombinedFieldsToMappings(mappings, [combinedField]), null, 2) + ); + this.props.onPipelineStringChange( + // @ts-expect-error + JSON.stringify(addCombinedFieldsToPipeline(pipeline, [combinedField]), null, 2) + ); + this.props.onCombinedFieldsChange([...this.props.combinedFields, combinedField]); + + this.closePopover(); + }; + + removeCombinedField = (index: number) => { + let mappings; + let pipeline; + try { + mappings = this.parseMappings(); + pipeline = this.parsePipeline(); + } catch (error) { + // how should remove error be surfaced? + return; + } + + const updatedCombinedFields = [...this.props.combinedFields]; + const removedCombinedFields = updatedCombinedFields.splice(index, 1); + + this.props.onMappingsStringChange( + // @ts-expect-error + JSON.stringify(removeCombinedFieldsFromMappings(mappings, removedCombinedFields), null, 2) + ); + this.props.onPipelineStringChange( + // @ts-expect-error + JSON.stringify(removeCombinedFieldsFromPipeline(pipeline, removedCombinedFields), null, 2) + ); + this.props.onCombinedFieldsChange(updatedCombinedFields); + }; + + parseMappings() { + try { + return JSON.parse(this.props.mappingsString); + } catch (error) { + throw new Error( + i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsForm.mappingsParseError', { + defaultMessage: 'Error parsing mappings: {error}', + values: { error: error.message }, + }) + ); + } + } + + parsePipeline() { + try { + return JSON.parse(this.props.pipelineString); + } catch (error) { + throw new Error( + i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsForm.pipelineParseError', { + defaultMessage: 'Error parsing pipeline: {error}', + values: { error: error.message }, + }) + ); + } + } + + hasNameCollision = (name: string) => { + if (this.props.results.column_names?.includes(name)) { + // collision with column name + return true; + } + + if ( + this.props.combinedFields.some((combinedField) => combinedField.combinedFieldName === name) + ) { + // collision with combined field name + return true; + } + + const mappings = this.parseMappings(); + return mappings.properties.hasOwnProperty(name); + }; + + render() { + const geoPointLabel = i18n.translate('xpack.ml.fileDatavisualizer.geoPointCombinedFieldLabel', { + defaultMessage: 'Add geo point field', + }); + const panels = [ + { + id: 0, + items: [ + { + name: geoPointLabel, + panel: 1, + }, + ], + }, + { + id: 1, + title: geoPointLabel, + content: ( + + ), + }, + ]; + return ( + +
+ {this.props.combinedFields.map((combinedField: CombinedField, idx: number) => ( + + + + + {!this.props.isDisabled && ( + + + + )} + + ))} + + + + } + isOpen={this.state.isPopoverOpen} + closePopover={this.closePopover} + anchorPosition="rightCenter" + > + + +
+
+ ); + } +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_read_only_form.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_read_only_form.tsx new file mode 100644 index 0000000000000..c37e27e39a7ab --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_read_only_form.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +import { EuiFormRow } from '@elastic/eui'; + +import { CombinedField } from './types'; +import { CombinedFieldLabel } from './combined_field_label'; + +export function CombinedFieldsReadOnlyForm({ + combinedFields, +}: { + combinedFields: CombinedField[]; +}) { + return combinedFields.length ? ( + +
+ {combinedFields.map((combinedField: CombinedField, idx: number) => ( + + ))} +
+
+ ) : null; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx new file mode 100644 index 0000000000000..831ae8de8081a --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import debounce from 'lodash/debounce'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { ChangeEvent, Component, Fragment } from 'react'; + +import { + EuiFormRow, + EuiFieldText, + EuiTextAlign, + EuiSpacer, + EuiButton, + EuiSelect, + EuiSelectOption, + EuiFormErrorText, +} from '@elastic/eui'; + +import { CombinedField } from './types'; +import { + createGeoPointCombinedField, + isWithinLatRange, + isWithinLonRange, + getFieldNames, + getNameCollisionMsg, +} from './utils'; +import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; + +interface Props { + addCombinedField: (combinedField: CombinedField) => void; + hasNameCollision: (name: string) => boolean; + results: FindFileStructureResponse; +} + +interface State { + latField: string; + lonField: string; + geoPointField: string; + geoPointFieldError: string; + latFields: EuiSelectOption[]; + lonFields: EuiSelectOption[]; + submitError: string; +} + +export class GeoPointForm extends Component { + constructor(props: Props) { + super(props); + + const latFields: EuiSelectOption[] = [{ value: '', text: '' }]; + const lonFields: EuiSelectOption[] = [{ value: '', text: '' }]; + getFieldNames(props.results).forEach((columnName: string) => { + if (isWithinLatRange(columnName, props.results.field_stats)) { + latFields.push({ value: columnName, text: columnName }); + } + if (isWithinLonRange(columnName, props.results.field_stats)) { + lonFields.push({ value: columnName, text: columnName }); + } + }); + + this.state = { + latField: '', + lonField: '', + geoPointField: '', + geoPointFieldError: '', + submitError: '', + latFields, + lonFields, + }; + } + + onLatFieldChange = (e: ChangeEvent) => { + this.setState({ latField: e.target.value }); + }; + + onLonFieldChange = (e: ChangeEvent) => { + this.setState({ lonField: e.target.value }); + }; + + onGeoPointFieldChange = (e: ChangeEvent) => { + const geoPointField = e.target.value; + this.setState({ geoPointField }); + this.hasNameCollision(geoPointField); + }; + + hasNameCollision = debounce((name: string) => { + try { + const geoPointFieldError = this.props.hasNameCollision(name) ? getNameCollisionMsg(name) : ''; + this.setState({ geoPointFieldError }); + } catch (error) { + this.setState({ submitError: error.message }); + } + }, 200); + + onSubmit = () => { + try { + this.props.addCombinedField( + createGeoPointCombinedField( + this.state.latField, + this.state.lonField, + this.state.geoPointField + ) + ); + this.setState({ submitError: '' }); + } catch (error) { + this.setState({ submitError: error.message }); + } + }; + + render() { + let error; + if (this.state.submitError) { + error = {this.state.submitError}; + } + return ( + + + + + + + + + + + + + + + + {error} + + + + + + + + ); + } +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/index.ts new file mode 100644 index 0000000000000..90b6bbab789f3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + addCombinedFieldsToPipeline, + addCombinedFieldsToMappings, + getDefaultCombinedFields, +} from './utils'; + +export { CombinedFieldsReadOnlyForm } from './combined_fields_read_only_form'; +export { CombinedFieldsForm } from './combined_fields_form'; +export { CombinedField } from './types'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/types.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/types.ts new file mode 100644 index 0000000000000..1ec66f5c96661 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface CombinedField { + mappingType: string; + delimiter: string; + combinedFieldName: string; + fieldNames: string[]; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.test.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.test.ts new file mode 100644 index 0000000000000..17b39f9041ec0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.test.ts @@ -0,0 +1,235 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + addCombinedFieldsToMappings, + addCombinedFieldsToPipeline, + createGeoPointCombinedField, + isWithinLatRange, + isWithinLonRange, + removeCombinedFieldsFromMappings, + removeCombinedFieldsFromPipeline, +} from './utils'; + +const combinedFields = [createGeoPointCombinedField('lat', 'lon', 'location')]; + +test('addCombinedFieldsToMappings', () => { + const mappings = { + _meta: { + created_by: '', + }, + properties: { + lat: { + type: 'number', + }, + lon: { + type: 'number', + }, + }, + }; + expect(addCombinedFieldsToMappings(mappings, combinedFields)).toEqual({ + _meta: { + created_by: '', + }, + properties: { + lat: { + type: 'number', + }, + lon: { + type: 'number', + }, + location: { + type: 'geo_point', + }, + }, + }); +}); + +test('removeCombinedFieldsFromMappings', () => { + const mappings = { + _meta: { + created_by: '', + }, + properties: { + lat: { + type: 'number', + }, + lon: { + type: 'number', + }, + location: { + type: 'geo_point', + }, + }, + }; + expect(removeCombinedFieldsFromMappings(mappings, combinedFields)).toEqual({ + _meta: { + created_by: '', + }, + properties: { + lat: { + type: 'number', + }, + lon: { + type: 'number', + }, + }, + }); +}); + +test('addCombinedFieldsToPipeline', () => { + const pipeline = { + description: '', + processors: [ + { + set: { + field: 'anotherfield', + value: '{{value}}', + }, + }, + ], + }; + expect(addCombinedFieldsToPipeline(pipeline, combinedFields)).toEqual({ + description: '', + processors: [ + { + set: { + field: 'anotherfield', + value: '{{value}}', + }, + }, + { + set: { + field: 'location', + value: '{{lat}},{{lon}}', + }, + }, + ], + }); +}); + +test('removeCombinedFieldsFromPipeline', () => { + const pipeline = { + description: '', + processors: [ + { + set: { + field: 'anotherfield', + value: '{{value}}', + }, + }, + { + set: { + field: 'location', + value: '{{lat}},{{lon}}', + }, + }, + ], + }; + expect(removeCombinedFieldsFromPipeline(pipeline, combinedFields)).toEqual({ + description: '', + processors: [ + { + set: { + field: 'anotherfield', + value: '{{value}}', + }, + }, + ], + }); +}); + +test('isWithinLatRange', () => { + expect(isWithinLatRange('fieldAlpha', {})).toBe(false); + expect( + isWithinLatRange('fieldAlpha', { + fieldAlpha: { + count: 1, + cardinality: 1, + top_hits: [{ count: 1, value: 1 }], + }, + }) + ).toBe(false); + expect( + isWithinLatRange('fieldAlpha', { + fieldAlpha: { + count: 1, + cardinality: 1, + top_hits: [{ count: 1, value: 100 }], + max_value: 100, + min_value: 0, + }, + }) + ).toBe(false); + expect( + isWithinLatRange('fieldAlpha', { + fieldAlpha: { + count: 1, + cardinality: 1, + top_hits: [{ count: 1, value: -100 }], + max_value: 0, + min_value: -100, + }, + }) + ).toBe(false); + expect( + isWithinLatRange('fieldAlpha', { + fieldAlpha: { + count: 1, + cardinality: 1, + top_hits: [{ count: 1, value: 0 }], + max_value: 0, + min_value: 0, + }, + }) + ).toBe(true); +}); + +test('isWithinLonRange', () => { + expect(isWithinLonRange('fieldAlpha', {})).toBe(false); + expect( + isWithinLonRange('fieldAlpha', { + fieldAlpha: { + count: 1, + cardinality: 1, + top_hits: [{ count: 1, value: 1 }], + }, + }) + ).toBe(false); + expect( + isWithinLonRange('fieldAlpha', { + fieldAlpha: { + count: 1, + cardinality: 1, + top_hits: [{ count: 1, value: 200 }], + max_value: 200, + min_value: 0, + }, + }) + ).toBe(false); + expect( + isWithinLonRange('fieldAlpha', { + fieldAlpha: { + count: 1, + cardinality: 1, + top_hits: [{ count: 1, value: -200 }], + max_value: 0, + min_value: -200, + }, + }) + ).toBe(false); + expect( + isWithinLonRange('fieldAlpha', { + fieldAlpha: { + count: 1, + cardinality: 1, + top_hits: [{ count: 1, value: 0 }], + max_value: 0, + min_value: 0, + }, + }) + ).toBe(true); +}); diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts new file mode 100644 index 0000000000000..5e7de14f451c2 --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; +import uuid from 'uuid/v4'; +import { CombinedField } from './types'; +import { + FindFileStructureResponse, + IngestPipeline, + Mappings, +} from '../../../../../../common/types/file_datavisualizer'; + +const COMMON_LAT_NAMES = ['latitude', 'lat']; +const COMMON_LON_NAMES = ['longitude', 'long', 'lon']; + +export function getDefaultCombinedFields(results: FindFileStructureResponse) { + const combinedFields: CombinedField[] = []; + const geoPointField = getGeoPointField(results); + if (geoPointField) { + combinedFields.push(geoPointField); + } + return combinedFields; +} + +export function addCombinedFieldsToMappings( + mappings: Mappings, + combinedFields: CombinedField[] +): Mappings { + const updatedMappings = { ...mappings }; + combinedFields.forEach((combinedField) => { + updatedMappings.properties[combinedField.combinedFieldName] = { + type: combinedField.mappingType, + }; + }); + return updatedMappings; +} + +export function removeCombinedFieldsFromMappings( + mappings: Mappings, + combinedFields: CombinedField[] +) { + const updatedMappings = { ...mappings }; + combinedFields.forEach((combinedField) => { + delete updatedMappings.properties[combinedField.combinedFieldName]; + }); + return updatedMappings; +} + +export function addCombinedFieldsToPipeline( + pipeline: IngestPipeline, + combinedFields: CombinedField[] +) { + const updatedPipeline = _.cloneDeep(pipeline); + combinedFields.forEach((combinedField) => { + updatedPipeline.processors.push({ + set: { + field: combinedField.combinedFieldName, + value: combinedField.fieldNames + .map((fieldName) => { + return `{{${fieldName}}}`; + }) + .join(combinedField.delimiter), + }, + }); + }); + return updatedPipeline; +} + +export function removeCombinedFieldsFromPipeline( + pipeline: IngestPipeline, + combinedFields: CombinedField[] +) { + return { + ...pipeline, + processors: pipeline.processors.filter((processor) => { + return 'set' in processor + ? !combinedFields.some((combinedField) => { + return processor.set.field === combinedField.combinedFieldName; + }) + : true; + }), + }; +} + +export function isWithinLatRange( + fieldName: string, + fieldStats: FindFileStructureResponse['field_stats'] +) { + return ( + fieldName in fieldStats && + 'max_value' in fieldStats[fieldName] && + fieldStats[fieldName]!.max_value! <= 90 && + 'min_value' in fieldStats[fieldName] && + fieldStats[fieldName]!.min_value! >= -90 + ); +} + +export function isWithinLonRange( + fieldName: string, + fieldStats: FindFileStructureResponse['field_stats'] +) { + return ( + fieldName in fieldStats && + 'max_value' in fieldStats[fieldName] && + fieldStats[fieldName]!.max_value! <= 180 && + 'min_value' in fieldStats[fieldName] && + fieldStats[fieldName]!.min_value! >= -180 + ); +} + +export function createGeoPointCombinedField( + latField: string, + lonField: string, + geoPointField: string +): CombinedField { + return { + mappingType: 'geo_point', + delimiter: ',', + combinedFieldName: geoPointField, + fieldNames: [latField, lonField], + }; +} + +export function getNameCollisionMsg(name: string) { + return i18n.translate('xpack.ml.fileDatavisualizer.nameCollisionMsg', { + defaultMessage: '"{name}" already exists, please provide a unique name', + values: { name }, + }); +} + +export function getFieldNames(results: FindFileStructureResponse): string[] { + return results.column_names !== undefined + ? results.column_names + : Object.keys(results.field_stats); +} + +function getGeoPointField(results: FindFileStructureResponse) { + const fieldNames = getFieldNames(results); + + const latField = fieldNames.find((columnName) => { + return ( + COMMON_LAT_NAMES.includes(columnName.toLowerCase()) && + isWithinLatRange(columnName, results.field_stats) + ); + }); + + const lonField = fieldNames.find((columnName) => { + return ( + COMMON_LON_NAMES.includes(columnName.toLowerCase()) && + isWithinLonRange(columnName, results.field_stats) + ); + }); + + if (!latField || !lonField) { + return null; + } + + const combinedFieldNames = [ + 'location', + 'point_location', + `${latField}_${lonField}`, + `location_${uuid()}`, + ]; + // Use first combinedFieldNames that does not have a naming collision + const geoPointField = combinedFieldNames.find((name) => { + return !fieldNames.includes(name); + }); + + return geoPointField ? createGeoPointCombinedField(latField, lonField, geoPointField) : null; +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.tsx index a79a7d36f3294..2b49746170f46 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.tsx @@ -17,7 +17,9 @@ import { EuiFlexItem, } from '@elastic/eui'; +import { CombinedField, CombinedFieldsForm } from '../combined_fields'; import { MLJobEditor, ML_EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor'; +import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer'; const EDITOR_HEIGHT = '300px'; interface Props { @@ -36,6 +38,9 @@ interface Props { onPipelineStringChange(): void; indexNameError: string; indexPatternNameError: string; + combinedFields: CombinedField[]; + onCombinedFieldsChange(combinedFields: CombinedField[]): void; + results: FindFileStructureResponse; } export const AdvancedSettings: FC = ({ @@ -54,6 +59,9 @@ export const AdvancedSettings: FC = ({ onPipelineStringChange, indexNameError, indexPatternNameError, + combinedFields, + onCombinedFieldsChange, + results, }) => { return ( @@ -123,6 +131,17 @@ export const AdvancedSettings: FC = ({ />
+ + = ({ @@ -46,6 +51,9 @@ export const ImportSettings: FC = ({ onPipelineStringChange, indexNameError, indexPatternNameError, + combinedFields, + onCombinedFieldsChange, + results, }) => { const tabs = [ { @@ -64,6 +72,7 @@ export const ImportSettings: FC = ({ createIndexPattern={createIndexPattern} onCreateIndexPatternChange={onCreateIndexPatternChange} indexNameError={indexNameError} + combinedFields={combinedFields} /> ), @@ -93,6 +102,9 @@ export const ImportSettings: FC = ({ onPipelineStringChange={onPipelineStringChange} indexNameError={indexNameError} indexPatternNameError={indexPatternNameError} + combinedFields={combinedFields} + onCombinedFieldsChange={onCombinedFieldsChange} + results={results} /> ), diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.tsx index 1e716824729e3..f6cd5909cbb80 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.tsx @@ -9,6 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; import { EuiFieldText, EuiFormRow, EuiCheckbox, EuiSpacer } from '@elastic/eui'; +import { CombinedField, CombinedFieldsReadOnlyForm } from '../combined_fields'; interface Props { index: string; @@ -17,6 +18,7 @@ interface Props { createIndexPattern: boolean; onCreateIndexPatternChange(): void; indexNameError: string; + combinedFields: CombinedField[]; } export const SimpleSettings: FC = ({ @@ -26,6 +28,7 @@ export const SimpleSettings: FC = ({ createIndexPattern, onCreateIndexPatternChange, indexNameError, + combinedFields, }) => { return ( @@ -75,6 +78,10 @@ export const SimpleSettings: FC = ({ onChange={onCreateIndexPatternChange} data-test-subj="mlFileDataVisCreateIndexPatternCheckbox" /> + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index 36b77a5a25e09..08b61a5fa4eed 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -26,6 +26,11 @@ import { ImportProgress, IMPORT_STATUS } from '../import_progress'; import { ImportErrors } from '../import_errors'; import { ImportSummary } from '../import_summary'; import { ImportSettings } from '../import_settings'; +import { + addCombinedFieldsToPipeline, + addCombinedFieldsToMappings, + getDefaultCombinedFields, +} from '../combined_fields'; import { ExperimentalBadge } from '../experimental_badge'; import { getIndexPatternNames, loadIndexPatterns } from '../../../../util/index_utils'; import { ml } from '../../../../services/ml_api_service'; @@ -68,6 +73,7 @@ const DEFAULT_STATE = { timeFieldName: undefined, isFilebeatFlyoutVisible: false, checkingValidIndex: false, + combinedFields: [], }; export class ImportView extends Component { @@ -386,6 +392,10 @@ export class ImportView extends Component { }); }; + onCombinedFieldsChange = (combinedFields) => { + this.setState({ combinedFields }); + }; + setImportProgress = (progress) => { this.setState({ uploadProgress: progress, @@ -444,6 +454,7 @@ export class ImportView extends Component { timeFieldName, isFilebeatFlyoutVisible, checkingValidIndex, + combinedFields, } = this.state; const createPipeline = pipelineString !== ''; @@ -513,6 +524,9 @@ export class ImportView extends Component { onPipelineStringChange={this.onPipelineStringChange} indexNameError={indexNameError} indexPatternNameError={indexPatternNameError} + combinedFields={combinedFields} + onCombinedFieldsChange={this.onCombinedFieldsChange} + results={this.props.results} /> @@ -644,12 +658,22 @@ function getDefaultState(state, results) { ? JSON.stringify(DEFAULT_INDEX_SETTINGS, null, 2) : state.indexSettingsString; + const combinedFields = state.combinedFields.length + ? state.combinedFields + : getDefaultCombinedFields(results); + const mappingsString = - state.mappingsString === '' ? JSON.stringify(results.mappings, null, 2) : state.mappingsString; + state.mappingsString === '' + ? JSON.stringify(addCombinedFieldsToMappings(results.mappings, combinedFields), null, 2) + : state.mappingsString; const pipelineString = state.pipelineString === '' && results.ingest_pipeline !== undefined - ? JSON.stringify(results.ingest_pipeline, null, 2) + ? JSON.stringify( + addCombinedFieldsToPipeline(results.ingest_pipeline, combinedFields), + null, + 2 + ) : state.pipelineString; const timeFieldName = results.timestamp_field; @@ -660,6 +684,7 @@ function getDefaultState(state, results) { mappingsString, pipelineString, timeFieldName, + combinedFields, }; } diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index d8e0c7274b549..8feef489fdde1 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -106,12 +106,17 @@ export class MlPlugin implements Plugin { const licensing = pluginsSetup.licensing.license$.pipe(take(1)); licensing.subscribe(async (license) => { const [coreStart] = await core.getStartServices(); + if (isMlEnabled(license)) { // add ML to home page if (pluginsSetup.home) { registerFeature(pluginsSetup.home); } + // the mlUrlGenerator should be registered even without full license + // for other plugins to access ML links + registerUrlGenerator(pluginsSetup.share, core); + const { capabilities } = coreStart.application; // register ML for the index pattern management no data screen. @@ -129,7 +134,6 @@ export class MlPlugin implements Plugin { } registerEmbeddables(pluginsSetup.embeddable, core); registerMlUiActions(pluginsSetup.uiActions, core); - registerUrlGenerator(pluginsSetup.share, core); } else if (managementApp) { managementApp.disable(); } diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index cf248fcc60896..7224eacf84e90 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -15,6 +15,7 @@ import { CapabilitiesStart, IClusterClient, } from 'kibana/server'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; import { MlCapabilities } from '../common/types/capabilities'; @@ -74,6 +75,7 @@ export class MlServerPlugin implements Plugin; + router: IRouter; features: FeaturesPluginSetup; elasticsearch: ElasticsearchServiceSetup; licensing: LicensingPluginSetup; - basePath: BasePath['get']; - router: IRouter; security?: SecurityPluginSetup; + spaces?: SpacesPluginSetup; } export interface ReportingInternalStart { @@ -50,7 +54,7 @@ export class ReportingCore { private exportTypesRegistry = getExportTypesRegistry(); private config?: ReportingConfig; - constructor() {} + constructor(private logger: LevelLogger) {} /* * Register setupDeps @@ -180,9 +184,9 @@ export class ReportingCore { return this.getPluginSetupDeps().elasticsearch; } - public async getSavedObjectsClient(fakeRequest: KibanaRequest) { + private async getSavedObjectsClient(request: KibanaRequest) { const { savedObjects } = await this.getPluginStartDeps(); - return savedObjects.getScopedClient(fakeRequest) as SavedObjectsClientContract; + return savedObjects.getScopedClient(request) as SavedObjectsClientContract; } public async getUiSettingsServiceFactory(savedObjectsClient: SavedObjectsClientContract) { @@ -190,4 +194,48 @@ export class ReportingCore { const scopedUiSettingsService = uiSettingsService.asScopedToClient(savedObjectsClient); return scopedUiSettingsService; } + + public getSpaceId(request: KibanaRequest): string | undefined { + const spacesService = this.getPluginSetupDeps().spaces?.spacesService; + if (spacesService) { + const spaceId = spacesService?.getSpaceId(request); + + if (spaceId !== DEFAULT_SPACE_ID) { + this.logger.info(`Request uses Space ID: ` + spaceId); + return spaceId; + } else { + this.logger.info(`Request uses default Space`); + } + } + } + + public getFakeRequest(baseRequest: object, spaceId?: string) { + const fakeRequest = KibanaRequest.from({ + path: '/', + route: { settings: {} }, + url: { href: '/' }, + raw: { req: { url: '/' } }, + ...baseRequest, + } as Hapi.Request); + + const spacesService = this.getPluginSetupDeps().spaces?.spacesService; + if (spacesService) { + if (spaceId && spaceId !== DEFAULT_SPACE_ID) { + this.logger.info(`Generating request for space: ` + spaceId); + this.getPluginSetupDeps().basePath.set(fakeRequest, `/s/${spaceId}`); + } + } + + return fakeRequest; + } + + public async getUiSettingsClient(request: KibanaRequest) { + const spacesService = this.getPluginSetupDeps().spaces?.spacesService; + const spaceId = this.getSpaceId(request); + if (spacesService && spaceId) { + this.logger.info(`Creating UI Settings Client for space: ${spaceId}`); + } + const savedObjectsClient = await this.getSavedObjectsClient(request); + return await this.getUiSettingsServiceFactory(savedObjectsClient); + } } diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts index 5ab029bfd9f29..4f0088467dd68 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts @@ -11,7 +11,6 @@ interface HasEncryptedHeaders { headers?: string; } -// TODO merge functionality with CSV execute job export const decryptJobHeaders = async < JobParamsType, TaskPayloadType extends HasEncryptedHeaders diff --git a/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts index cb792fbd6ae03..0b06beabfd24d 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts @@ -7,7 +7,7 @@ import { getAbsoluteUrlFactory } from './get_absolute_url'; const defaultOptions = { - defaultBasePath: 'sbp', + basePath: 'sbp', protocol: 'http:', hostname: 'localhost', port: 5601, @@ -64,8 +64,8 @@ test(`uses the provided hash with queryString`, () => { }); test(`uses the provided basePath`, () => { - const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions); - const absoluteUrl = getAbsoluteUrl({ basePath: '/s/marketing' }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ ...defaultOptions, basePath: '/s/marketing' }); + const absoluteUrl = getAbsoluteUrl(); expect(absoluteUrl).toBe(`http://localhost:5601/s/marketing/app/kibana`); }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts index f996a49e5eadc..72305f47e7189 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts @@ -7,7 +7,7 @@ import url from 'url'; interface AbsoluteURLFactoryOptions { - defaultBasePath: string; + basePath: string; protocol: string; hostname: string; port: string | number; @@ -17,14 +17,9 @@ export const getAbsoluteUrlFactory = ({ protocol, hostname, port, - defaultBasePath, + basePath, }: AbsoluteURLFactoryOptions) => { - return function getAbsoluteUrl({ - basePath = defaultBasePath, - hash = '', - path = '/app/kibana', - search = '', - } = {}) { + return function getAbsoluteUrl({ hash = '', path = '/app/kibana', search = '' } = {}) { return url.format({ protocol, hostname, diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts index a0d8ff0852544..794ea9febb5c0 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts @@ -5,24 +5,16 @@ */ import { ReportingConfig } from '../../'; -import { ReportingCore } from '../../core'; -import { - createMockConfig, - createMockConfigSchema, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfig, createMockConfigSchema } from '../../test_helpers'; import { BasePayload } from '../../types'; -import { TaskPayloadPDF } from '../printable_pdf/types'; -import { getConditionalHeaders, getCustomLogo } from './'; +import { getConditionalHeaders } from './'; let mockConfig: ReportingConfig; -let mockReportingPlugin: ReportingCore; beforeEach(async () => { const reportingConfig = { kibanaServer: { hostname: 'custom-hostname' } }; const mockSchema = createMockConfigSchema(reportingConfig); mockConfig = createMockConfig(mockSchema); - mockReportingPlugin = await createMockReportingCore(mockConfig); }); describe('conditions', () => { @@ -32,7 +24,7 @@ describe('conditions', () => { baz: 'quix', }; - const conditionalHeaders = await getConditionalHeaders({ + const conditionalHeaders = getConditionalHeaders({ job: {} as BasePayload, filteredHeaders: permittedHeaders, config: mockConfig, @@ -51,83 +43,6 @@ describe('conditions', () => { }); }); -test('uses basePath from job when creating saved object service', async () => { - const mockGetSavedObjectsClient = jest.fn(); - mockReportingPlugin.getSavedObjectsClient = mockGetSavedObjectsClient; - - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - const conditionalHeaders = await getConditionalHeaders({ - job: {} as BasePayload, - filteredHeaders: permittedHeaders, - config: mockConfig, - }); - const jobBasePath = '/sbp/s/marketing'; - await getCustomLogo({ - reporting: mockReportingPlugin, - job: { basePath: jobBasePath } as TaskPayloadPDF, - conditionalHeaders, - config: mockConfig, - }); - - const getBasePath = mockGetSavedObjectsClient.mock.calls[0][0].getBasePath; - expect(getBasePath()).toBe(jobBasePath); -}); - -test(`uses basePath from server if job doesn't have a basePath when creating saved object service`, async () => { - const mockGetSavedObjectsClient = jest.fn(); - mockReportingPlugin.getSavedObjectsClient = mockGetSavedObjectsClient; - - const reportingConfig = { kibanaServer: { hostname: 'localhost' }, server: { basePath: '/sbp' } }; - const mockSchema = createMockConfigSchema(reportingConfig); - mockConfig = createMockConfig(mockSchema); - - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - const conditionalHeaders = await getConditionalHeaders({ - job: {} as BasePayload, - filteredHeaders: permittedHeaders, - config: mockConfig, - }); - - await getCustomLogo({ - reporting: mockReportingPlugin, - job: {} as TaskPayloadPDF, - conditionalHeaders, - config: mockConfig, - }); - - const getBasePath = mockGetSavedObjectsClient.mock.calls[0][0].getBasePath; - expect(getBasePath()).toBe(`/sbp`); - expect(mockGetSavedObjectsClient.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "getBasePath": [Function], - "headers": Object { - "baz": "quix", - "foo": "bar", - }, - "path": "/", - "raw": Object { - "req": Object { - "url": "/", - }, - }, - "route": Object { - "settings": Object {}, - }, - "url": Object { - "href": "/", - }, - }, - ] - `); -}); - describe('config formatting', () => { test(`lowercases kibanaServer.hostname`, async () => { const reportingConfig = { kibanaServer: { hostname: 'GREAT-HOSTNAME' } }; diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts deleted file mode 100644 index ee61d76c8a933..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ReportingConfig, ReportingCore } from '../../'; -import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; -import { ConditionalHeaders } from '../../types'; -import { TaskPayloadPDF } from '../printable_pdf/types'; // Logo is PDF only - -export const getCustomLogo = async ({ - reporting, - config, - job, - conditionalHeaders, -}: { - reporting: ReportingCore; - config: ReportingConfig; - job: TaskPayloadPDF; - conditionalHeaders: ConditionalHeaders; -}) => { - const serverBasePath: string = config.kbnConfig.get('server', 'basePath'); - const fakeRequest: any = { - headers: conditionalHeaders.headers, - // This is used by the spaces SavedObjectClientWrapper to determine the existing space. - // We use the basePath from the saved job, which we'll have post spaces being implemented; - // or we use the server base path, which uses the default space - getBasePath: () => job.basePath || serverBasePath, - path: '/', - route: { settings: {} }, - url: { href: '/' }, - raw: { req: { url: '/' } }, - }; - - const savedObjectsClient = await reporting.getSavedObjectsClient(fakeRequest); - const uiSettings = await reporting.getUiSettingsServiceFactory(savedObjectsClient); - const logo: string = await uiSettings.get(UI_SETTINGS_CUSTOM_PDF_LOGO); - return { conditionalHeaders, logo }; -}; diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts index d6f472e18bc7b..f4e3a7b723c08 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts @@ -36,12 +36,7 @@ export function getFullUrls({ config.get('kibanaServer', 'hostname'), config.get('kibanaServer', 'port'), ] as string[]; - const getAbsoluteUrl = getAbsoluteUrlFactory({ - defaultBasePath: basePath, - protocol, - hostname, - port, - }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ basePath, protocol, hostname, port }); // PDF and PNG job params put in the url differently let relativeUrls: string[] = []; @@ -61,7 +56,6 @@ export function getFullUrls({ const urls = relativeUrls.map((relativeUrl) => { const parsedRelative: UrlWithStringQuery = urlParse(relativeUrl); const jobUrl = getAbsoluteUrl({ - basePath: job.basePath, path: parsedRelative.pathname, hash: parsedRelative.hash, search: parsedRelative.search, diff --git a/x-pack/plugins/reporting/server/export_types/common/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts index e0d03eb4864ca..80eaa52d0951b 100644 --- a/x-pack/plugins/reporting/server/export_types/common/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/index.ts @@ -6,7 +6,6 @@ export { decryptJobHeaders } from './decrypt_job_headers'; export { getConditionalHeaders } from './get_conditional_headers'; -export { getCustomLogo } from './get_custom_logo'; export { getFullUrls } from './get_full_urls'; export { omitBlockedHeaders } from './omit_blocked_headers'; export { validateUrls } from './validate_urls'; diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index be18bd7fff361..d768dc6f8e084 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -25,6 +25,7 @@ export const createJobFnFactory: CreateJobFnFactory { - const decryptHeaders = async () => { - try { - if (typeof headers !== 'string') { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage', - { - defaultMessage: 'Job headers are missing', - } - ) - ); - } - return await crypto.decrypt(headers); - } catch (err) { - logger.error(err); - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage', - { - defaultMessage: 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}', - values: { encryptionKey: 'xpack.reporting.encryptionKey', err: err.toString() }, - } - ) - ); // prettier-ignore - } - }; - - return KibanaRequest.from({ - headers: await decryptHeaders(), - // This is used by the spaces SavedObjectClientWrapper to determine the existing space. - // We use the basePath from the saved job, which we'll have post spaces being implemented; - // or we use the server base path, which uses the default space - path: '/', - route: { settings: {} }, - url: { href: '/' }, - app: {}, - raw: { req: { url: '/' } }, - } as Hapi.Request); -}; - export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); return async function runTask(jobId, job, cancellationToken) { @@ -67,16 +21,15 @@ export const runTaskFnFactory: RunTaskFnFactory callAsCurrentUser(endpoint, clientParams, options); - const savedObjectsClient = await reporting.getSavedObjectsClient(fakeRequest); - const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); - const { content, maxSizeReached, size, csvContainsFormulas, warnings } = await generateCsv( job, config, diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts index 915d5010a4885..1f3354debc305 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts @@ -6,6 +6,10 @@ import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'kibana/server'; +import { + UI_SETTINGS_CSV_QUOTE_VALUES, + UI_SETTINGS_CSV_SEPARATOR, +} from '../../../../common/constants'; import { ReportingConfig } from '../../../'; import { LevelLogger } from '../../../lib'; @@ -38,8 +42,8 @@ export const getUiSettings = async ( // Separator, QuoteValues const [separator, quoteValues] = await Promise.all([ - client.get('csv:separator'), - client.get('csv:quoteValues'), + client.get(UI_SETTINGS_CSV_SEPARATOR), + client.get(UI_SETTINGS_CSV_QUOTE_VALUES), ]); return { diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts index e383f21143149..6ecddae12a988 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts @@ -37,9 +37,7 @@ interface SearchRequest { } export interface GenerateCsvParams { - jobParams: { - browserTimezone: string; - }; + browserTimezone: string; searchRequest: SearchRequest; indexPatternSavedObject: IndexPatternSavedObject; fields: string[]; @@ -57,12 +55,7 @@ export function createGenerateCsv(logger: LevelLogger) { callEndpoint: EndpointCaller, cancellationToken: CancellationToken ): Promise { - const settings = await getUiSettings( - job.jobParams?.browserTimezone, - uiSettingsClient, - config, - logger - ); + const settings = await getUiSettings(job.browserTimezone, uiSettingsClient, config, logger); const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues); const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; const builder = new MaxSizeStringBuilder(byteSizeValueToNumber(settings.maxSizeBytes), bom); diff --git a/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts b/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts deleted file mode 100644 index 09e6becc2baec..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Crypto } from '@elastic/node-crypto'; -import { i18n } from '@kbn/i18n'; -import Hapi from 'hapi'; -import { KibanaRequest } from '../../../../../../../src/core/server'; -import { LevelLogger } from '../../../lib'; - -export const getRequest = async ( - headers: string | undefined, - crypto: Crypto, - logger: LevelLogger -) => { - const decryptHeaders = async () => { - try { - if (typeof headers !== 'string') { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage', - { - defaultMessage: 'Job headers are missing', - } - ) - ); - } - return await crypto.decrypt(headers); - } catch (err) { - logger.error(err); - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage', - { - defaultMessage: 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}', - values: { encryptionKey: 'xpack.reporting.encryptionKey', err: err.toString() }, - } - ) - ); // prettier-ignore - } - }; - - return KibanaRequest.from({ - headers: await decryptHeaders(), - // This is used by the spaces SavedObjectClientWrapper to determine the existing space. - // We use the basePath from the saved job, which we'll have post spaces being implemented; - // or we use the server base path, which uses the default space - path: '/', - route: { settings: {} }, - url: { href: '/' }, - raw: { req: { url: '/' } }, - } as Hapi.Request); -}; diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts index f420d8b033170..214157db51cb7 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts @@ -29,6 +29,7 @@ export interface IndexPatternSavedObject { } export interface JobParamsDiscoverCsv extends BaseParams { + browserTimezone: string; indexPatternId: string; title: string; searchRequest: SearchRequest; @@ -38,6 +39,7 @@ export interface JobParamsDiscoverCsv extends BaseParams { } export interface TaskPayloadCSV extends BasePayload { + browserTimezone: string; basePath: string; searchRequest: any; fields: any; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index 3a5deda176b8c..0ca80581fcc83 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -48,9 +48,8 @@ export const runTaskFnFactory: RunTaskFnFactory = function e jobLogger.debug(`Execute job generating [${visType}] csv`); const savedObjectsClient = context.core.savedObjects.client; - - const uiConfig = await reporting.getUiSettingsServiceFactory(savedObjectsClient); - const job = await getGenerateCsvParams(jobParams, panel, savedObjectsClient, uiConfig); + const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); + const job = await getGenerateCsvParams(jobParams, panel, savedObjectsClient, uiSettingsClient); const elasticsearch = reporting.getElasticsearchService(); const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req); @@ -58,7 +57,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const { content, maxSizeReached, size, csvContainsFormulas, warnings } = await generateCsv( job, config, - uiConfig, + uiSettingsClient, callAsCurrentUser, new CancellationToken() // can not be cancelled ); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts index 9646d7eecd5b5..b387245406fbb 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts @@ -45,6 +45,7 @@ describe('Get CSV Job', () => { ); expect(result).toMatchInlineSnapshot(` Object { + "browserTimezone": "PST", "conflictedTypesFields": Array [], "fields": Array [], "indexPatternSavedObject": Object { @@ -57,9 +58,6 @@ describe('Get CSV Job', () => { "timeFieldName": null, "title": null, }, - "jobParams": Object { - "browserTimezone": "PST", - }, "metaFields": Array [], "searchRequest": Object { "body": Object { @@ -99,6 +97,7 @@ describe('Get CSV Job', () => { ); expect(result).toMatchInlineSnapshot(` Object { + "browserTimezone": "PST", "conflictedTypesFields": Array [], "fields": Array [], "indexPatternSavedObject": Object { @@ -111,9 +110,6 @@ describe('Get CSV Job', () => { "timeFieldName": null, "title": null, }, - "jobParams": Object { - "browserTimezone": "PST", - }, "metaFields": Array [], "searchRequest": Object { "body": Object { @@ -156,6 +152,7 @@ describe('Get CSV Job', () => { ); expect(result).toMatchInlineSnapshot(` Object { + "browserTimezone": "Africa/Timbuktu", "conflictedTypesFields": Array [], "fields": Array [], "indexPatternSavedObject": Object { @@ -168,9 +165,6 @@ describe('Get CSV Job', () => { "timeFieldName": null, "title": null, }, - "jobParams": Object { - "browserTimezone": "Africa/Timbuktu", - }, "metaFields": Array [], "searchRequest": Object { "body": Object { @@ -212,6 +206,7 @@ describe('Get CSV Job', () => { ); expect(result).toMatchInlineSnapshot(` Object { + "browserTimezone": "Africa/Timbuktu", "conflictedTypesFields": Array [], "fields": Array [ "@test_time", @@ -226,9 +221,6 @@ describe('Get CSV Job', () => { "timeFieldName": "@test_time", "title": "test search", }, - "jobParams": Object { - "browserTimezone": "Africa/Timbuktu", - }, "metaFields": Array [], "searchRequest": Object { "body": Object { @@ -286,6 +278,7 @@ describe('Get CSV Job', () => { ); expect(result).toMatchInlineSnapshot(` Object { + "browserTimezone": "Africa/Timbuktu", "conflictedTypesFields": Array [], "fields": Array [ "@test_time", @@ -300,9 +293,6 @@ describe('Get CSV Job', () => { "timeFieldName": "@test_time", "title": "test search", }, - "jobParams": Object { - "browserTimezone": "Africa/Timbuktu", - }, "metaFields": Array [], "searchRequest": Object { "body": Object { diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts index 0fc29c5b208d9..26a4b17aaf71f 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts @@ -12,6 +12,8 @@ import { IIndexPattern, Query, } from '../../../../../../../src/plugins/data/server'; +import { TimeRangeParams } from '../../../types'; +import { GenerateCsvParams } from '../../csv/generate_csv'; import { DocValueFields, IndexPatternField, @@ -23,7 +25,6 @@ import { } from '../types'; import { getDataSource } from './get_data_source'; import { getFilters } from './get_filters'; -import { GenerateCsvParams } from '../../csv/generate_csv'; export const getEsQueryConfig = async (config: IUiSettingsClient) => { const configs = await Promise.all([ @@ -49,7 +50,7 @@ export const getGenerateCsvParams = async ( savedObjectsClient: SavedObjectsClientContract, uiConfig: IUiSettingsClient ): Promise => { - let timerange; + let timerange: TimeRangeParams; if (jobParams.post?.timerange) { timerange = jobParams.post?.timerange; } else { @@ -136,7 +137,7 @@ export const getGenerateCsvParams = async ( }; return { - jobParams: { browserTimezone: timerange.timezone }, + browserTimezone: timerange.timezone, indexPatternSavedObject, searchRequest, fields: includes, diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index 173a67ad18edf..3727b2ec7b432 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -25,13 +25,13 @@ export const createJobFnFactory: CreateJobFnFactory { - basePath?: string; browserTimezone: string; forceNow?: string; layout: LayoutParams; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index 96e634337e6a9..cae706a479b7f 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -25,10 +25,10 @@ export const createJobFnFactory: CreateJobFnFactory>; @@ -42,9 +42,7 @@ export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFac mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), map((decryptedHeaders) => omitBlockedHeaders({ job, decryptedHeaders })), map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), - mergeMap((conditionalHeaders) => - getCustomLogo({ reporting, config, job, conditionalHeaders }) - ), + mergeMap((conditionalHeaders) => getCustomLogo(reporting, conditionalHeaders, job.spaceId)), mergeMap(({ logo, conditionalHeaders }) => { const urls = getFullUrls({ config, job }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts similarity index 74% rename from x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts index ec4e54632eef5..8fa8fa5cbe3cb 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingConfig, ReportingCore } from '../../'; +import { ReportingConfig, ReportingCore } from '../../../'; import { createMockConfig, createMockConfigSchema, createMockReportingCore, -} from '../../test_helpers'; -import { TaskPayloadPDF } from '../printable_pdf/types'; -import { getConditionalHeaders, getCustomLogo } from './'; +} from '../../../test_helpers'; +import { getConditionalHeaders } from '../../common'; +import { TaskPayloadPDF } from '../types'; +import { getCustomLogo } from './get_custom_logo'; let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; @@ -38,18 +39,13 @@ test(`gets logo from uiSettings`, async () => { get: mockGet, }); - const conditionalHeaders = await getConditionalHeaders({ + const conditionalHeaders = getConditionalHeaders({ job: {} as TaskPayloadPDF, filteredHeaders: permittedHeaders, config: mockConfig, }); - const { logo } = await getCustomLogo({ - reporting: mockReportingPlugin, - config: mockConfig, - job: {} as TaskPayloadPDF, - conditionalHeaders, - }); + const { logo } = await getCustomLogo(mockReportingPlugin, conditionalHeaders); expect(mockGet).toBeCalledWith('xpackReporting:customPdfLogo'); expect(logo).toBe('purple pony'); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts new file mode 100644 index 0000000000000..35ab7001ecbe4 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReportingCore } from '../../../'; +import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; +import { ConditionalHeaders } from '../../../types'; + +export const getCustomLogo = async ( + reporting: ReportingCore, + conditionalHeaders: ConditionalHeaders, + spaceId?: string +) => { + const fakeRequest = reporting.getFakeRequest({ headers: conditionalHeaders.headers }, spaceId); + const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest); + + const logo: string = await uiSettingsClient.get(UI_SETTINGS_CUSTOM_PDF_LOGO); + + // continue the pipeline + return { conditionalHeaders, logo }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index 3020cbb5f28b0..7fd176e71f2d5 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -16,7 +16,6 @@ export interface JobParamsPDF extends BaseParams { // Job payload: structure of stored job data provided by create_job export interface TaskPayloadPDF extends BasePayload { - basePath?: string; browserTimezone: string; forceNow?: string; layout: LayoutParams; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 0aae8b567bcdb..03d88ca60e2c0 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -12,6 +12,7 @@ import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../type import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; import { Report } from './report'; + interface JobSettings { timeout: number; browser_type: string; diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index adb89abe20280..6a93a35bfcc84 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -34,7 +34,7 @@ export class ReportingPlugin constructor(context: PluginInitializerContext) { this.logger = new LevelLogger(context.logger.get()); this.initializerContext = context; - this.reportingCore = new ReportingCore(); + this.reportingCore = new ReportingCore(this.logger); } public setup(core: CoreSetup, plugins: ReportingSetupDeps) { @@ -70,11 +70,11 @@ export class ReportingPlugin }); const { elasticsearch, http } = core; - const { features, licensing, security } = plugins; + const { features, licensing, security, spaces } = plugins; const { initializerContext: initContext, reportingCore } = this; const router = http.createRouter(); - const basePath = http.basePath.get; + const basePath = http.basePath; reportingCore.pluginSetup({ features, @@ -83,6 +83,7 @@ export class ReportingPlugin basePath, router, security, + spaces, }); registerReportingUsageCollector(reportingCore, plugins); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index 979283f9f037c..0acf384869ded 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -35,19 +35,8 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log config.get('kibanaServer', 'port'), ] as string[]; - const getAbsoluteUrl = getAbsoluteUrlFactory({ - defaultBasePath: basePath, - protocol, - hostname, - port, - }); - - const hashUrl = getAbsoluteUrl({ - basePath, - path: '/', - hash: '', - search: '', - }); + const getAbsoluteUrl = getAbsoluteUrlFactory({ basePath, protocol, hostname, port }); + const hashUrl = getAbsoluteUrl({ path: '/', hash: '', search: '' }); // Hack the layout to make the base/login page work const layout = { diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 6ec35db5caec6..72772f9f7b755 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -37,18 +37,19 @@ const createMockPluginSetup = ( return { features: featuresPluginMock.createSetup(), elasticsearch: setupMock.elasticsearch || { legacy: { client: {} } }, - basePath: setupMock.basePath || '/all-about-that-basepath', + basePath: { set: jest.fn() }, router: setupMock.router, security: setupMock.security, licensing: { license$: Rx.of({ isAvailable: true, isActive: true, type: 'basic' }) } as any, }; }; +const logger = createMockLevelLogger(); + const createMockPluginStart = ( mockReportingCore: ReportingCore, startMock?: any ): ReportingInternalStart => { - const logger = createMockLevelLogger(); const store = new ReportingStore(mockReportingCore, logger); return { browserDriverFactory: startMock.browserDriverFactory, @@ -134,7 +135,7 @@ export const createMockReportingCore = async ( } config = config || {}; - const core = new ReportingCore(); + const core = new ReportingCore(logger); core.pluginSetup(setupDepsMock); core.setConfig(config); diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index c67a95c2de754..a3c63a0fb539d 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -8,6 +8,7 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { SpacesPluginSetup } from '../../spaces/server'; import { CancellationToken } from '../../../plugins/reporting/common'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -53,6 +54,7 @@ export interface BasePayload { jobParams: JobParamsType; title: string; type: string; + spaceId?: string; } export interface JobSource { @@ -95,6 +97,7 @@ export interface ReportingSetupDeps { licensing: LicensingPluginSetup; features: FeaturesPluginSetup; security?: SecurityPluginSetup; + spaces?: SpacesPluginSetup; usageCollection?: UsageCollectionSetup; } @@ -121,7 +124,6 @@ export interface BaseParams { } export interface BaseParamsEncryptedFields extends BaseParams { - basePath?: string; // for screenshot type reports headers: string; // encrypted headers } diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 213bea3bc3eec..1211d4c2cf1c3 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; import { ReportingConfig } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { GetLicense } from './'; @@ -118,7 +118,7 @@ async function handleResponse(response: SearchResponse): Promise { const reportingIndex = config.get('index'); diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts index 100d09a2da7e4..8f26579726ff1 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -5,7 +5,7 @@ */ import { first, map } from 'rxjs/operators'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingCore } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; @@ -36,7 +36,7 @@ export function getReportingUsageCollector( ) { return usageCollection.makeUsageCollector({ type: 'reporting', - fetch: (callCluster: CallCluster) => { + fetch: (callCluster: LegacyAPICaller) => { const config = reporting.getConfig(); return getReportingUsage(config, getLicense, callCluster, exportTypesRegistry); }, diff --git a/x-pack/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts index aa06d3f696d00..daacc065629a4 100644 --- a/x-pack/plugins/rollup/server/collectors/register.ts +++ b/x-pack/plugins/rollup/server/collectors/register.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; interface IdToFlagMap { [key: string]: boolean; @@ -27,7 +27,7 @@ function createIdToFlagMap(ids: string[]) { }, {} as any); } -async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCluster) { +async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: LegacyAPICaller) { const searchParams = { size: ES_MAX_RESULT_WINDOW_DEFAULT_VALUE, index: kibanaIndex, @@ -56,7 +56,7 @@ async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCl async function fetchRollupSavedSearches( kibanaIndex: string, - callCluster: CallCluster, + callCluster: LegacyAPICaller, rollupIndexPatternToFlagMap: IdToFlagMap ) { const searchParams = { @@ -104,7 +104,7 @@ async function fetchRollupSavedSearches( async function fetchRollupVisualizations( kibanaIndex: string, - callCluster: CallCluster, + callCluster: LegacyAPICaller, rollupIndexPatternToFlagMap: IdToFlagMap, rollupSavedSearchesToFlagMap: IdToFlagMap ) { @@ -211,7 +211,7 @@ export function registerRollupUsageCollector( total: { type: 'long' }, }, }, - fetch: async (callCluster: CallCluster) => { + fetch: async (callCluster: LegacyAPICaller) => { const rollupIndexPatterns = await fetchRollupIndexPatterns(kibanaIndex, callCluster); const rollupIndexPatternToFlagMap = createIdToFlagMap(rollupIndexPatterns); diff --git a/x-pack/plugins/searchprofiler/server/plugin.ts b/x-pack/plugins/searchprofiler/server/plugin.ts index 0dfb65aa6f857..032593d5e3b31 100644 --- a/x-pack/plugins/searchprofiler/server/plugin.ts +++ b/x-pack/plugins/searchprofiler/server/plugin.ts @@ -20,10 +20,9 @@ export class SearchProfilerServerPlugin implements Plugin { this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { licensing, elasticsearch }: AppServerPluginDependencies) { + async setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { const router = http.createRouter(); profileRoute.register({ - elasticsearch, router, getLicenseStatus: () => this.licenseStatus, log: this.log, diff --git a/x-pack/plugins/searchprofiler/server/types.ts b/x-pack/plugins/searchprofiler/server/types.ts index 7aa0032afba13..84733b0ccfd95 100644 --- a/x-pack/plugins/searchprofiler/server/types.ts +++ b/x-pack/plugins/searchprofiler/server/types.ts @@ -5,18 +5,15 @@ */ import { IRouter, Logger } from 'kibana/server'; -import { ElasticsearchPlugin } from '../../../../src/legacy/core_plugins/elasticsearch'; import { LicensingPluginSetup } from '../../licensing/server'; import { LicenseStatus } from '../common'; export interface AppServerPluginDependencies { licensing: LicensingPluginSetup; - elasticsearch: ElasticsearchPlugin; } export interface RouteDependencies { getLicenseStatus: () => LicenseStatus; - elasticsearch: ElasticsearchPlugin; router: IRouter; log: Logger; } diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts index 2b78355787ff2..1bab51e70a494 100644 --- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts +++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts @@ -21,6 +21,7 @@ export const createFeature = ( icon: 'discoverApp', navLinkId: 'discover', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: [], privileges: privileges === null diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index bf791b37087bd..7dff2912e6aa3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -32,6 +32,7 @@ const buildFeatures = () => { name: 'Feature 1', icon: 'addDataApp', app: ['feature1App'], + category: { id: 'foo', label: 'foo' }, privileges: { all: { app: ['feature1App'], @@ -56,6 +57,7 @@ const buildFeatures = () => { name: 'Feature 2', icon: 'addDataApp', app: ['feature2App'], + category: { id: 'foo', label: 'foo' }, privileges: { all: { app: ['feature2App'], diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx index 7ecf32ee45b85..77b6da2a00487 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx @@ -18,6 +18,7 @@ const buildProps = (customProps: any = {}) => { id: 'feature1', name: 'Feature 1', app: ['app'], + category: { id: 'foo', label: 'foo' }, icon: 'spacesApp', privileges: { all: { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx index bc60613345910..0242fddc957c9 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -28,6 +28,7 @@ const features = [ id: 'normal', name: 'normal feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { all: [], read: [] }, @@ -43,6 +44,7 @@ const features = [ id: 'normal_with_sub', name: 'normal feature with sub features', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { all: [], read: [] }, @@ -96,6 +98,7 @@ const features = [ id: 'bothPrivilegesExcludedFromBase', name: 'bothPrivilegesExcludedFromBase', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { excludeFromBasePrivileges: true, @@ -113,6 +116,7 @@ const features = [ id: 'allPrivilegeExcludedFromBase', name: 'allPrivilegeExcludedFromBase', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { excludeFromBasePrivileges: true, diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index 98faae6edab2c..ea24560c8ddc9 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -80,6 +80,7 @@ describe('usingPrivileges', () => { id: 'fooFeature', name: 'Foo KibanaFeature', app: ['fooApp', 'foo'], + category: { id: 'foo', label: 'foo' }, navLinkId: 'foo', privileges: null, }), @@ -168,6 +169,7 @@ describe('usingPrivileges', () => { id: 'fooFeature', name: 'Foo KibanaFeature', app: ['foo'], + category: { id: 'foo', label: 'foo' }, navLinkId: 'foo', privileges: null, }), @@ -322,6 +324,7 @@ describe('usingPrivileges', () => { name: 'Foo KibanaFeature', navLinkId: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }), new KibanaFeature({ @@ -329,6 +332,7 @@ describe('usingPrivileges', () => { name: 'Bar KibanaFeature', navLinkId: 'bar', app: ['bar'], + category: { id: 'foo', label: 'foo' }, privileges: null, }), ], @@ -469,6 +473,7 @@ describe('usingPrivileges', () => { name: 'Foo KibanaFeature', navLinkId: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }), new KibanaFeature({ @@ -476,6 +481,7 @@ describe('usingPrivileges', () => { name: 'Bar KibanaFeature', navLinkId: 'bar', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }), ], @@ -552,6 +558,7 @@ describe('all', () => { id: 'fooFeature', name: 'Foo KibanaFeature', app: ['foo'], + category: { id: 'foo', label: 'foo' }, navLinkId: 'foo', privileges: null, }), diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index dc261e2eec982..5f19c911fd5d3 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -33,6 +33,7 @@ describe(`feature_privilege_builder`, () => { id: 'my-feature', name: 'my-feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: privilege, read: privilege, @@ -64,6 +65,7 @@ describe(`feature_privilege_builder`, () => { id: 'my-feature', name: 'my-feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: privilege, read: privilege, @@ -101,6 +103,7 @@ describe(`feature_privilege_builder`, () => { id: 'my-feature', name: 'my-feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: privilege, read: privilege, @@ -148,6 +151,7 @@ describe(`feature_privilege_builder`, () => { id: 'my-feature', name: 'my-feature', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: privilege, read: privilege, diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts index 033040fd2f14b..bdf2c87f40f0b 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -14,6 +14,7 @@ describe('featurePrivilegeIterator', () => { name: 'foo', privileges: null, app: [], + category: { id: 'foo', label: 'foo' }, }); const actualPrivileges = Array.from( @@ -29,6 +30,7 @@ describe('featurePrivilegeIterator', () => { const feature = new KibanaFeature({ id: 'foo', name: 'foo', + category: { id: 'foo', label: 'foo' }, privileges: { all: { api: ['all-api', 'read-api'], @@ -120,6 +122,7 @@ describe('featurePrivilegeIterator', () => { const feature = new KibanaFeature({ id: 'foo', name: 'foo', + category: { id: 'foo', label: 'foo' }, privileges: { all: { api: ['all-api', 'read-api'], @@ -194,6 +197,7 @@ describe('featurePrivilegeIterator', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { api: ['all-api', 'read-api'], @@ -317,6 +321,7 @@ describe('featurePrivilegeIterator', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { api: ['all-api', 'read-api'], @@ -440,6 +445,7 @@ describe('featurePrivilegeIterator', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { api: ['all-api', 'read-api'], @@ -567,6 +573,7 @@ describe('featurePrivilegeIterator', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { api: ['all-api', 'read-api'], @@ -690,6 +697,7 @@ describe('featurePrivilegeIterator', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { api: ['all-api', 'read-api'], @@ -815,6 +823,7 @@ describe('featurePrivilegeIterator', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -923,6 +932,7 @@ describe('featurePrivilegeIterator', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { api: ['all-api', 'read-api'], diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index dd8ac44386dbd..6f721c91fbd67 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -21,6 +21,7 @@ describe('features', () => { icon: 'arrowDown', navLinkId: 'kibana:foo', app: ['app-1', 'app-2'], + category: { id: 'foo', label: 'foo' }, catalogue: ['catalogue-1', 'catalogue-2'], management: { foo: ['management-1', 'management-2'], @@ -66,6 +67,7 @@ describe('features', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -165,6 +167,7 @@ describe('features', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }), ]; @@ -207,6 +210,7 @@ describe('features', () => { icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['ignore-me-1', 'ignore-me-2'], management: { foo: ['ignore-me-1', 'ignore-me-2'], @@ -327,6 +331,7 @@ describe('features', () => { icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['ignore-me-1', 'ignore-me-2'], management: { foo: ['ignore-me-1', 'ignore-me-2'], @@ -409,6 +414,7 @@ describe('features', () => { icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['ignore-me-1', 'ignore-me-2'], management: { foo: ['ignore-me-1', 'ignore-me-2'], @@ -467,6 +473,7 @@ describe('features', () => { icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['ignore-me-1', 'ignore-me-2'], management: { foo: ['ignore-me-1', 'ignore-me-2'], @@ -532,6 +539,7 @@ describe('features', () => { icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], + category: { id: 'foo', label: 'foo' }, catalogue: ['ignore-me-1', 'ignore-me-2'], management: { foo: ['ignore-me-1', 'ignore-me-2'], @@ -602,6 +610,7 @@ describe('reserved', () => { icon: 'arrowDown', navLinkId: 'kibana:foo', app: ['app-1', 'app-2'], + category: { id: 'foo', label: 'foo' }, catalogue: ['catalogue-1', 'catalogue-2'], management: { foo: ['management-1', 'management-2'], @@ -644,6 +653,7 @@ describe('reserved', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { privileges: [ @@ -708,6 +718,7 @@ describe('reserved', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -749,6 +760,7 @@ describe('subFeatures', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -876,6 +888,7 @@ describe('subFeatures', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -1075,6 +1088,7 @@ describe('subFeatures', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, excludeFromBasePrivileges: true, privileges: { all: { @@ -1216,6 +1230,7 @@ describe('subFeatures', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -1379,6 +1394,7 @@ describe('subFeatures', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, excludeFromBasePrivileges: true, privileges: { all: { @@ -1508,6 +1524,7 @@ describe('subFeatures', () => { name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { diff --git a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts index 8e6d72670c8d9..d449eb29d53d8 100644 --- a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts @@ -12,6 +12,7 @@ it('allows features to be defined without privileges', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }); @@ -23,6 +24,7 @@ it('allows features with reserved privileges to be defined', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'foo', @@ -49,6 +51,7 @@ it('allows features with sub-features to be defined', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -112,6 +115,7 @@ it('does not allow features with sub-features which have id conflicts with the m id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -162,6 +166,7 @@ it('does not allow features with sub-features which have id conflicts with the p id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { @@ -212,6 +217,7 @@ it('does not allow features with sub-features which have id conflicts each other id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { savedObject: { diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts index d91a4d4151316..0c7d12f67f4b9 100644 --- a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts @@ -13,6 +13,7 @@ it('allows features to be defined without privileges', () => { name: 'foo', app: [], privileges: null, + category: { id: 'foo', label: 'foo' }, }); validateReservedPrivileges([feature]); @@ -23,6 +24,7 @@ it('allows features with a single reserved privilege to be defined', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'foo', @@ -49,6 +51,7 @@ it('allows multiple features with reserved privileges to be defined', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'foo', @@ -71,6 +74,7 @@ it('allows multiple features with reserved privileges to be defined', () => { id: 'foo2', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'foo', @@ -97,6 +101,7 @@ it('prevents a feature from specifying the same reserved privilege id', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'foo', @@ -135,6 +140,7 @@ it('prevents features from sharing a reserved privilege id', () => { id: 'foo', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'foo', @@ -157,6 +163,7 @@ it('prevents features from sharing a reserved privilege id', () => { id: 'foo2', name: 'foo', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, reserved: { description: 'foo', diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 6e9b88f30479f..811ea080b4316 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -87,6 +87,7 @@ const putRoleTest = ( id: 'feature_1', name: 'feature 1', app: [], + category: { id: 'foo', label: 'foo' }, privileges: { all: { ui: [], diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index f002e13a07cf1..5fbba84467ecf 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -299,6 +299,7 @@ export const type = t.keyof({ query: null, saved_query: null, threshold: null, + threat_match: null, }); export type Type = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts index b666b95ea1e97..777256ff961f9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts @@ -48,3 +48,104 @@ export const getAddPrepackagedRulesSchemaDecodedMock = (): AddPrepackagedRulesSc exceptions_list: [], rule_id: 'rule-1', }); + +export const getAddPrepackagedThreatMatchRulesSchemaMock = (): AddPrepackagedRulesSchema => ({ + description: 'some description', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + version: 1, + threat_query: '*:*', + threat_index: 'list-index', + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); + +export const getAddPrepackagedThreatMatchRulesSchemaDecodedMock = (): AddPrepackagedRulesSchemaDecoded => ({ + author: [], + description: 'some description', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + severity_mapping: [], + type: 'threat_match', + risk_score: 55, + risk_score_mapping: [], + language: 'kuery', + references: [], + actions: [], + enabled: false, + false_positives: [], + from: 'now-6m', + interval: '5m', + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + to: 'now', + threat: [], + throttle: null, + version: 1, + exceptions_list: [], + rule_id: 'rule-1', + threat_query: '*:*', + threat_index: 'list-index', + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index 9b90cf9fdf782..69538f025d95d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -45,6 +45,12 @@ import { RiskScoreMapping, SeverityMapping, } from '../common/schemas'; +import { + threat_index, + threat_query, + threat_filters, + threat_mapping, +} from '../types/threat_mapping'; import { DefaultStringArray, @@ -116,6 +122,10 @@ export const addPrepackagedRulesSchema = t.intersection([ references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode exceptions_list: DefaultListArray, // defaults to empty array if not set during decode + threat_filters, // defaults to "undefined" if not set during decode + threat_mapping, // defaults to "undefined" if not set during decode + threat_query, // defaults to "undefined" if not set during decode + threat_index, // defaults to "undefined" if not set during decode }) ), ]); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts index 137b40eb648ba..8c916e4f013b4 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts @@ -17,6 +17,8 @@ import { left } from 'fp-ts/lib/Either'; import { getAddPrepackagedRulesSchemaMock, getAddPrepackagedRulesSchemaDecodedMock, + getAddPrepackagedThreatMatchRulesSchemaMock, + getAddPrepackagedThreatMatchRulesSchemaDecodedMock, } from './add_prepackaged_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; import { getListArrayMock } from '../types/lists.mock'; @@ -1597,4 +1599,16 @@ describe('add prepackaged rules schema', () => { expect(message.schema).toEqual(expected); }); }); + + describe('threat_mapping', () => { + test('You can set a threat query, index, mapping, filters on a pre-packaged rule', () => { + const payload = getAddPrepackagedThreatMatchRulesSchemaMock(); + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getAddPrepackagedThreatMatchRulesSchemaDecodedMock(); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts index f1e87bdb11e75..32299be500b45 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts @@ -55,3 +55,103 @@ export const getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ( exceptions_list: [], rule_id: 'rule-1', }); + +export const getCreateThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, + threat_query: '*:*', + threat_index: 'list-index', + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); + +export const getCreateThreatMatchRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({ + author: [], + severity_mapping: [], + risk_score_mapping: [], + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + references: [], + actions: [], + enabled: true, + false_positives: [], + from: 'now-6m', + interval: '5m', + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + to: 'now', + threat: [], + throttle: null, + version: 1, + exceptions_list: [], + rule_id: 'rule-1', + threat_query: '*:*', + threat_index: 'list-index', + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts index 56bc68a275ee4..19517017743f1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts @@ -16,6 +16,8 @@ import { left } from 'fp-ts/lib/Either'; import { getCreateRulesSchemaMock, getCreateRulesSchemaDecodedMock, + getCreateThreatMatchRulesSchemaMock, + getCreateThreatMatchRulesSchemaDecodedMock, } from './create_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; import { getListArrayMock } from '../types/lists.mock'; @@ -1661,4 +1663,16 @@ describe('create rules schema', () => { expect(message.schema).toEqual(expected); }); }); + + describe('threat_mapping', () => { + test('You can set a threat query, index, mapping, filters when creating a rule', () => { + const payload = getCreateThreatMatchRulesSchemaMock(); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getCreateThreatMatchRulesSchemaDecodedMock(); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts index 7b6b98383cc33..c024ba1c48f8d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts @@ -46,6 +46,12 @@ import { RiskScoreMapping, SeverityMapping, } from '../common/schemas'; +import { + threat_index, + threat_query, + threat_filters, + threat_mapping, +} from '../types/threat_mapping'; import { DefaultStringArray, @@ -112,6 +118,10 @@ export const createRulesSchema = t.intersection([ note, // defaults to "undefined" if not set during decode version: DefaultVersionNumber, // defaults to 1 if not set during decode exceptions_list: DefaultListArray, // defaults to empty array if not set during decode + threat_mapping, // defaults to "undefined" if not set during decode + threat_query, // defaults to "undefined" if not set during decode + threat_filters, // defaults to "undefined" if not set during decode + threat_index, // defaults to "undefined" if not set during decode }) ), ]); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts index 43f0901912271..75ad92578318c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getCreateRulesSchemaMock } from './create_rules_schema.mock'; +import { + getCreateRulesSchemaMock, + getCreateThreatMatchRulesSchemaMock, +} from './create_rules_schema.mock'; import { CreateRulesSchema } from './create_rules_schema'; import { createRuleValidateTypeDependents } from './create_rules_type_dependents'; @@ -87,4 +90,39 @@ describe('create_rules_type_dependents', () => { const errors = createRuleValidateTypeDependents(schema); expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); }); + + test('threat_index, threat_query, and threat_mapping are required when type is "threat_match" and validates with it', () => { + const schema: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), + type: 'threat_match', + }; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual([ + 'when "type" is "threat_match", "threat_index" is required', + 'when "type" is "threat_match", "threat_query" is required', + 'when "type" is "threat_match", "threat_mapping" is required', + ]); + }); + + test('validates with threat_index, threat_query, and threat_mapping when type is "threat_match"', () => { + const schema = getCreateThreatMatchRulesSchemaMock(); + const { threat_filters: threatFilters, ...noThreatFilters } = schema; + const errors = createRuleValidateTypeDependents(noThreatFilters); + expect(errors).toEqual([]); + }); + + test('does NOT validate when threat_mapping is an empty array', () => { + const schema: CreateRulesSchema = { + ...getCreateThreatMatchRulesSchemaMock(), + threat_mapping: [], + }; + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual(['threat_mapping" must have at least one element']); + }); + + test('validates with threat_index, threat_query, threat_mapping, and an optional threat_filters, when type is "threat_match"', () => { + const schema = getCreateThreatMatchRulesSchemaMock(); + const errors = createRuleValidateTypeDependents(schema); + expect(errors).toEqual([]); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts index 91b14fa9b999c..c2a41005ebf4d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts @@ -5,7 +5,7 @@ */ import { isMlRule } from '../../../machine_learning/helpers'; -import { isThresholdRule } from '../../utils'; +import { isThreatMatchRule, isThresholdRule } from '../../utils'; import { CreateRulesSchema } from './create_rules_schema'; export const validateAnomalyThreshold = (rule: CreateRulesSchema): string[] => { @@ -107,6 +107,24 @@ export const validateThreshold = (rule: CreateRulesSchema): string[] => { return []; }; +export const validateThreatMapping = (rule: CreateRulesSchema): string[] => { + let errors: string[] = []; + if (isThreatMatchRule(rule.type)) { + if (!rule.threat_mapping) { + errors = ['when "type" is "threat_match", "threat_mapping" is required', ...errors]; + } else if (rule.threat_mapping.length === 0) { + errors = ['threat_mapping" must have at least one element', ...errors]; + } + if (!rule.threat_query) { + errors = ['when "type" is "threat_match", "threat_query" is required', ...errors]; + } + if (!rule.threat_index) { + errors = ['when "type" is "threat_match", "threat_index" is required', ...errors]; + } + } + return errors; +}; + export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): string[] => { return [ ...validateAnomalyThreshold(schema), @@ -117,5 +135,6 @@ export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): str ...validateTimelineId(schema), ...validateTimelineTitle(schema), ...validateThreshold(schema), + ...validateThreatMapping(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts index e3b4196c90c6c..160dbb92b74cd 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts @@ -76,3 +76,94 @@ export const ruleIdsToNdJsonString = (ruleIds: string[]) => { const rules = ruleIds.map((ruleId) => getImportRulesSchemaMock(ruleId)); return rulesToNdJsonString(rules); }; + +export const getImportThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): ImportRulesSchema => ({ + description: 'some description', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, + threat_index: 'index-123', + threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }], + threat_query: '*:*', + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); + +export const getImportThreatMatchRulesSchemaDecodedMock = (): ImportRulesSchemaDecoded => ({ + author: [], + description: 'some description', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + severity_mapping: [], + type: 'threat_match', + risk_score: 55, + risk_score_mapping: [], + language: 'kuery', + references: [], + actions: [], + enabled: true, + false_positives: [], + from: 'now-6m', + interval: '5m', + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + to: 'now', + threat: [], + throttle: null, + version: 1, + exceptions_list: [], + rule_id: 'rule-1', + immutable: false, + threat_query: '*:*', + threat_index: 'index-123', + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts index 0515bee0052d7..bd25a63e153dd 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts @@ -20,6 +20,8 @@ import { import { getImportRulesSchemaMock, getImportRulesSchemaDecodedMock, + getImportThreatMatchRulesSchemaMock, + getImportThreatMatchRulesSchemaDecodedMock, } from './import_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; import { getListArrayMock } from '../types/lists.mock'; @@ -1792,4 +1794,16 @@ describe('import rules schema', () => { expect(message.schema).toEqual(expected); }); }); + + describe('threat_mapping', () => { + test('You can set a threat query, index, mapping, filters on an imported rule', () => { + const payload = getImportThreatMatchRulesSchemaMock(); + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getImportThreatMatchRulesSchemaDecodedMock(); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index 698716fea696e..b63d70783b7b5 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -52,6 +52,12 @@ import { RiskScoreMapping, SeverityMapping, } from '../common/schemas'; +import { + threat_index, + threat_query, + threat_filters, + threat_mapping, +} from '../types/threat_mapping'; import { DefaultStringArray, @@ -135,6 +141,10 @@ export const importRulesSchema = t.intersection([ updated_at, // defaults "undefined" if not set during decode created_by, // defaults "undefined" if not set during decode updated_by, // defaults "undefined" if not set during decode + threat_filters, // defaults to "undefined" if not set during decode + threat_mapping, // defaults to "undefined" if not set during decode + threat_query, // defaults to "undefined" if not set during decode + threat_index, // defaults to "undefined" if not set during decode }) ), ]); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index ed9fb8930ea1b..a462b297d37f8 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -82,3 +82,31 @@ export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSch machine_learning_job_id: 'some_machine_learning_job_id', }; }; + +export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { + return { + ...getRulesSchemaMock(anchorDate), + type: 'threat_match', + threat_index: 'index-123', + threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }], + threat_query: '*:*', + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], + }; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts index 36fc063761840..3a47d4af6ac14 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts @@ -17,11 +17,16 @@ import { addQueryFields, addTimelineTitle, addMlFields, + addThreatMatchFields, } from './rules_schema'; import { exactCheck } from '../../../exact_check'; import { foldLeftRight, getPaths } from '../../../test_utils'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; -import { getRulesSchemaMock, getRulesMlSchemaMock } from './rules_schema.mocks'; +import { + getRulesSchemaMock, + getRulesMlSchemaMock, + getThreatMatchingSchemaMock, +} from './rules_schema.mocks'; import { ListArray } from '../types/lists'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; @@ -593,6 +598,36 @@ describe('rules_schema', () => { expect(getPaths(left(message.errors))).toEqual(['invalid keys "query,language"']); expect(message.schema).toEqual({}); }); + + test('it validates a threat_match response', () => { + const payload = getThreatMatchingSchemaMock(); + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getThreatMatchingSchemaMock(); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it rejects a response with threat_match properties but type of "query"', () => { + const payload: RulesSchema = { + ...getThreatMatchingSchemaMock(), + type: 'query', + }; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'invalid keys "threat_index,threat_mapping,[{"entries":[{"field":"host.name","type":"mapping","value":"host.name"}]}],threat_query,threat_filters,[{"bool":{"must":[{"query_string":{"query":"host.name: linux","analyze_wildcard":true,"time_zone":"Zulu"}}],"filter":[],"should":[],"must_not":[]}}]"', + ]); + expect(message.schema).toEqual({}); + }); }); describe('addSavedId', () => { @@ -647,6 +682,11 @@ describe('rules_schema', () => { const fields = addQueryFields({ type: 'saved_query' }); expect(fields.length).toEqual(2); }); + + test('should return two fields for a rule of type "threat_match"', () => { + const fields = addQueryFields({ type: 'threat_match' }); + expect(fields.length).toEqual(2); + }); }); describe('addMlFields', () => { @@ -704,4 +744,17 @@ describe('rules_schema', () => { expect(message.schema).toEqual({ ...payload, exceptions_list: [] }); }); }); + + describe('addThreatMatchFields', () => { + test('should return empty array if type is not "threat_match"', () => { + const fields = addThreatMatchFields({ type: 'query' }); + const expected: t.Mixed[] = []; + expect(fields).toEqual(expected); + }); + + test('should return 5 fields for a rule of type "threat_match"', () => { + const fields = addThreatMatchFields({ type: 'threat_match' }); + expect(fields.length).toEqual(5); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index c26a7efb0c288..1c2254f9f8f09 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -60,6 +60,13 @@ import { rule_name_override, timestamp_override, } from '../common/schemas'; +import { + threat_index, + threat_query, + threat_filters, + threat_mapping, +} from '../types/threat_mapping'; + import { DefaultListArray } from '../types/lists_default_array'; import { DefaultStringArray, @@ -114,7 +121,7 @@ export const dependentRulesSchema = t.partial({ language, query, - // when type = saved_query, saved_is is required + // when type = saved_query, saved_id is required saved_id, // These two are required together or not at all. @@ -127,6 +134,12 @@ export const dependentRulesSchema = t.partial({ // Threshold fields threshold, + + // Threat Match fields + threat_filters, + threat_index, + threat_query, + threat_mapping, }); /** @@ -206,7 +219,9 @@ export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mi }; export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (['eql', 'query', 'saved_query', 'threshold'].includes(typeAndTimelineOnly.type)) { + if ( + ['eql', 'query', 'saved_query', 'threshold', 'threat_match'].includes(typeAndTimelineOnly.type) + ) { return [ t.exact(t.type({ query: dependentRulesSchema.props.query })), t.exact(t.type({ language: dependentRulesSchema.props.language })), @@ -240,6 +255,20 @@ export const addThresholdFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t. } }; +export const addThreatMatchFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'threat_match') { + return [ + t.exact(t.type({ threat_query: dependentRulesSchema.props.threat_query })), + t.exact(t.type({ threat_index: dependentRulesSchema.props.threat_index })), + t.exact(t.type({ threat_mapping: dependentRulesSchema.props.threat_mapping })), + t.exact(t.partial({ threat_filters: dependentRulesSchema.props.threat_filters })), + t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })), + ]; + } else { + return []; + } +}; + export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { const dependents: t.Mixed[] = [ t.exact(requiredRulesSchema), @@ -249,6 +278,7 @@ export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed ...addQueryFields(typeAndTimelineOnly), ...addMlFields(typeAndTimelineOnly), ...addThresholdFields(typeAndTimelineOnly), + ...addThreatMatchFields(typeAndTimelineOnly), ]; if (dependents.length > 1) { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts new file mode 100644 index 0000000000000..63d593ea84e67 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ThreatMapping, + threatMappingEntries, + ThreatMappingEntries, + threat_mapping, +} from './threat_mapping'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { exactCheck } from '../../../exact_check'; + +describe('threat_mapping', () => { + describe('threatMappingEntries', () => { + test('it should validate an entry', () => { + const payload: ThreatMappingEntries = [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + }, + ]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate an extra entry item', () => { + const payload: ThreatMappingEntries & Array<{ extra: string }> = [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + extra: 'blah', + }, + ]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a non string', () => { + const payload = ([ + { + field: 5, + type: 'mapping', + value: 'field.one', + }, + ] as unknown) as ThreatMappingEntries[]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a wrong type', () => { + const payload = ([ + { + field: 'field.one', + type: 'invalid', + value: 'field.one', + }, + ] as unknown) as ThreatMappingEntries[]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('threat_mapping', () => { + test('it should validate a threat mapping', () => { + const payload: ThreatMapping = [ + { + entries: [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + }, + ], + }, + ]; + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + test('it should NOT validate an extra key', () => { + const payload: ThreatMapping & Array<{ extra: string }> = [ + { + entries: [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + }, + ], + extra: 'invalid', + }, + ]; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an extra inner entry', () => { + const payload: ThreatMapping & Array<{ entries: Array<{ extra: string }> }> = [ + { + entries: [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + extra: 'blah', + }, + ], + }, + ]; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an extra inner entry with the wrong data type', () => { + const payload = ([ + { + entries: [ + { + field: 5, + type: 'mapping', + value: 'field.one', + }, + ], + }, + ] as unknown) as ThreatMapping; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "entries,field"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts new file mode 100644 index 0000000000000..f2b4754c2d113 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { NonEmptyString } from './non_empty_string'; + +export const threat_query = t.string; +export type ThreatQuery = t.TypeOf; +export const threatQueryOrUndefined = t.union([threat_query, t.undefined]); +export type ThreatQueryOrUndefined = t.TypeOf; + +export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet +export type ThreatFilters = t.TypeOf; +export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]); +export type ThreatFiltersOrUndefined = t.TypeOf; + +export const threatMappingEntries = t.array( + t.exact( + t.type({ + field: NonEmptyString, + type: t.keyof({ mapping: null }), + value: NonEmptyString, + }) + ) +); +export type ThreatMappingEntries = t.TypeOf; + +export const threat_mapping = t.array( + t.exact( + t.type({ + entries: threatMappingEntries, + }) + ) +); +export type ThreatMapping = t.TypeOf; + +export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]); +export type ThreatMappingOrUndefined = t.TypeOf; + +export const threat_index = t.string; +export const threatIndexOrUndefined = t.union([threat_index, t.undefined]); +export type ThreatIndexOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts index 99680ffe41d44..ea50acc9b46be 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { hasLargeValueList, hasNestedEntry } from './utils'; +import { hasLargeValueList, hasNestedEntry, isThreatMatchRule } from './utils'; import { EntriesArray } from '../shared_imports'; describe('#hasLargeValueList', () => { @@ -102,4 +102,14 @@ describe('#hasNestedEntry', () => { expect(hasLists).toBeFalsy(); }); + + describe('isThreatMatchRule', () => { + test('it returns true if a threat match rule', () => { + expect(isThreatMatchRule('threat_match')).toEqual(true); + }); + + test('it returns false if not a threat match rule', () => { + expect(isThreatMatchRule('query')).toEqual(false); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 170d28cb5a725..f76417099bb17 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -17,6 +17,7 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => { return found.length > 0; }; -export const isEqlRule = (ruleType: Type | undefined) => ruleType === 'eql'; -export const isThresholdRule = (ruleType: Type | undefined) => ruleType === 'threshold'; -export const isQueryRule = (ruleType: Type | undefined) => ruleType === 'query'; +export const isEqlRule = (ruleType: Type | undefined): boolean => ruleType === 'eql'; +export const isThresholdRule = (ruleType: Type | undefined): boolean => ruleType === 'threshold'; +export const isQueryRule = (ruleType: Type | undefined): boolean => ruleType === 'query'; +export const isThreatMatchRule = (ruleType: Type): boolean => ruleType === 'threat_match'; diff --git a/x-pack/plugins/security_solution/cypress/.eslintrc.json b/x-pack/plugins/security_solution/cypress/.eslintrc.json index 96a5a52f13e6c..a738652e2d27b 100644 --- a/x-pack/plugins/security_solution/cypress/.eslintrc.json +++ b/x-pack/plugins/security_solution/cypress/.eslintrc.json @@ -2,5 +2,8 @@ "plugins": ["cypress"], "env": { "cypress/globals": true + }, + "rules": { + "import/no-extraneous-dependencies": "off" } } diff --git a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts index 6438a738580b7..e09d62d2a87d1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts @@ -28,7 +28,7 @@ import { resetFields, } from '../tasks/fields_browser'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { openTimelineFieldsBrowser, populateTimeline } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; @@ -48,7 +48,7 @@ describe('Fields Browser', () => { context('Fields Browser rendering', () => { before(() => { loginAndWaitForPage(HOSTS_URL); - openTimeline(); + openTimelineUsingToggle(); populateTimeline(); openTimelineFieldsBrowser(); }); @@ -111,7 +111,7 @@ describe('Fields Browser', () => { context('Editing the timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); - openTimeline(); + openTimelineUsingToggle(); populateTimeline(); openTimelineFieldsBrowser(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts index 53ddff501db82..c19e51c3ada40 100644 --- a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts @@ -12,7 +12,7 @@ import { import { closesModal, openStatsAndTables } from '../tasks/inspect'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { executeTimelineKQL, openTimelineInspectButton, @@ -58,7 +58,7 @@ describe('Inspect', () => { it('inspects the timeline', () => { const hostExistsQuery = 'host.name: *'; loginAndWaitForPage(HOSTS_URL); - openTimeline(); + openTimelineUsingToggle(); executeTimelineKQL(hostExistsQuery); openTimelineSettings(); openTimelineInspectButton(); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts new file mode 100644 index 0000000000000..9f61d11b7ac0f --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { timeline } from '../objects/timeline'; + +import { + FAVORITE_TIMELINE, + LOCKED_ICON, + NOTES, + NOTES_BUTTON, + NOTES_COUNT, + NOTES_TEXT_AREA, + PIN_EVENT, + TIMELINE_DESCRIPTION, + // TIMELINE_FILTER, + TIMELINE_QUERY, + TIMELINE_TITLE, +} from '../screens/timeline'; +import { + TIMELINES_DESCRIPTION, + TIMELINES_PINNED_EVENT_COUNT, + TIMELINES_NOTES_COUNT, + TIMELINES_FAVORITE, +} from '../screens/timelines'; + +import { loginAndWaitForPage } from '../tasks/login'; +import { openTimelineUsingToggle } from '../tasks/security_main'; +import { + addDescriptionToTimeline, + addFilter, + addNameToTimeline, + addNotesToTimeline, + closeNotes, + closeTimeline, + createNewTimeline, + markAsFavorite, + openTimelineFromSettings, + pinFirstEvent, + populateTimeline, + waitForTimelineChanges, +} from '../tasks/timeline'; +import { openTimeline } from '../tasks/timelines'; + +import { OVERVIEW_URL } from '../urls/navigation'; + +describe('Timelines', () => { + before(() => { + cy.server(); + cy.route('PATCH', '**/api/timeline').as('timeline'); + }); + + it('Creates a timeline', async () => { + loginAndWaitForPage(OVERVIEW_URL); + openTimelineUsingToggle(); + populateTimeline(); + addFilter(timeline.filter); + pinFirstEvent(); + + cy.get(PIN_EVENT).should('have.attr', 'aria-label', 'Pinned event'); + cy.get(LOCKED_ICON).should('be.visible'); + + addNameToTimeline(timeline.title); + + const response = await cy.wait('@timeline').promisify(); + const timelineId = JSON.parse(response.xhr.responseText).data.persistTimeline.timeline + .savedObjectId; + + addDescriptionToTimeline(timeline.description); + addNotesToTimeline(timeline.notes); + closeNotes(); + markAsFavorite(); + waitForTimelineChanges(); + createNewTimeline(); + closeTimeline(); + openTimelineFromSettings(); + + cy.contains(timeline.title).should('exist'); + cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); + cy.get(TIMELINES_PINNED_EVENT_COUNT).first().should('have.text', '1'); + cy.get(TIMELINES_NOTES_COUNT).first().should('have.text', '1'); + cy.get(TIMELINES_FAVORITE).first().should('exist'); + + openTimeline(timelineId); + + cy.get(FAVORITE_TIMELINE).should('exist'); + cy.get(TIMELINE_TITLE).should('have.attr', 'value', timeline.title); + cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', timeline.description); + cy.get(TIMELINE_QUERY).should('have.text', timeline.query); + // Comments this assertion until we agreed what to do with the filters. + // cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); + cy.get(NOTES_COUNT).should('have.text', '1'); + cy.get(PIN_EVENT).should('have.attr', 'aria-label', 'Pinned event'); + cy.get(NOTES_BUTTON).click(); + cy.get(NOTES_TEXT_AREA).should('have.attr', 'placeholder', 'Add a Note'); + cy.get(NOTES).should('have.text', timeline.notes); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts index df0a26f3649c0..f62db083172a4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts @@ -19,7 +19,7 @@ import { } from '../tasks/hosts/all_hosts'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { createNewTimeline } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; @@ -31,7 +31,7 @@ describe('timeline data providers', () => { }); beforeEach(() => { - openTimeline(); + openTimelineUsingToggle(); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_events.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_events.spec.ts deleted file mode 100644 index 549cd134a04a4..0000000000000 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_events.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PIN_EVENT } from '../screens/timeline'; - -import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; -import { pinFirstEvent, populateTimeline, unpinFirstEvent } from '../tasks/timeline'; - -import { HOSTS_URL } from '../urls/navigation'; - -describe('timeline events', () => { - before(() => { - loginAndWaitForPage(HOSTS_URL); - openTimeline(); - populateTimeline(); - }); - - after(() => { - unpinFirstEvent(); - }); - - it('pins the first event to the timeline', () => { - cy.server(); - cy.route('POST', '**/api/solutions/security/graphql').as('persistTimeline'); - - pinFirstEvent(); - - cy.wait('@persistTimeline', { timeout: 10000 }).then((response) => { - cy.wrap(response.status).should('eql', 200); - cy.wrap(response.xhr.responseText).should('include', 'persistPinnedEventOnTimeline'); - }); - - cy.get(PIN_EVENT).should('have.attr', 'aria-label', 'Pinned event'); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts index 87639f41d4109..9b3434b5521d4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts @@ -8,7 +8,7 @@ import { TIMELINE_FLYOUT_HEADER, TIMELINE_NOT_READY_TO_DROP_BUTTON } from '../sc import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline, openTimelineIfClosed } from '../tasks/security_main'; +import { openTimelineUsingToggle, openTimelineIfClosed } from '../tasks/security_main'; import { createNewTimeline } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; @@ -25,7 +25,7 @@ describe('timeline flyout button', () => { }); it('toggles open the timeline', () => { - openTimeline(); + openTimelineUsingToggle(); cy.get(TIMELINE_FLYOUT_HEADER).should('have.css', 'visibility', 'visible'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts index a2e2a72a17946..814fcee2b0c5f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts @@ -7,7 +7,7 @@ import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { executeTimelineKQL } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; @@ -19,7 +19,7 @@ describe('timeline search or filter KQL bar', () => { it('executes a KQL query', () => { const hostExistsQuery = 'host.name: *'; - openTimeline(); + openTimelineUsingToggle(); executeTimelineKQL(hostExistsQuery); cy.get(SERVER_SIDE_EVENT_COUNT) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts index 12e6f3db9b61e..e4f303fb89fda 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts @@ -12,7 +12,7 @@ import { } from '../screens/timeline'; import { loginAndWaitForPage } from '../tasks/login'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { checkIdToggleField, createNewTimeline, @@ -30,7 +30,7 @@ describe('toggle column in timeline', () => { }); beforeEach(() => { - openTimeline(); + openTimelineUsingToggle(); populateTimeline(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts index d8f96aaf5e563..103bbaad8f303 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exportTimeline, waitForTimelinesPanelToBeLoaded } from '../tasks/timeline'; +import { exportTimeline, waitForTimelinesPanelToBeLoaded } from '../tasks/timelines'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 6d605e1d577a9..6c1d73492f30a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -30,7 +30,7 @@ import { openAllHosts } from '../tasks/hosts/main'; import { waitForIpsTableToBeLoaded } from '../tasks/network/flows'; import { clearSearchBar, kqlSearch, navigateFromHeaderTo } from '../tasks/security_header'; -import { openTimeline } from '../tasks/security_main'; +import { openTimelineUsingToggle } from '../tasks/security_main'; import { addDescriptionToTimeline, addNameToTimeline, @@ -82,7 +82,7 @@ describe('url state', () => { it('sets the timeline start and end dates from the url when locked to global time', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url); - openTimeline(); + openTimelineUsingToggle(); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', @@ -105,7 +105,7 @@ describe('url state', () => { ); cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON).should('have.attr', 'title', ABSOLUTE_DATE.endTime); - openTimeline(); + openTimelineUsingToggle(); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).should( 'have.attr', @@ -121,7 +121,7 @@ describe('url state', () => { it('sets the url state when timeline/global date pickers are unlinked and timeline start and end date are set', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlUnlinked); - openTimeline(); + openTimelineUsingToggle(); setTimelineStartDate(ABSOLUTE_DATE.newStartTimeTyped); updateTimelineDates(); setTimelineEndDate(ABSOLUTE_DATE.newEndTimeTyped); @@ -220,7 +220,7 @@ describe('url state', () => { it('sets and reads the url state for timeline by id', () => { loginAndWaitForPage(HOSTS_URL); - openTimeline(); + openTimelineUsingToggle(); executeTimelineKQL('host.name: *'); cy.get(SERVER_SIDE_EVENT_COUNT) diff --git a/x-pack/plugins/security_solution/cypress/objects/timeline.ts b/x-pack/plugins/security_solution/cypress/objects/timeline.ts index ff7e80e5661ad..6121cb9a99b14 100644 --- a/x-pack/plugins/security_solution/cypress/objects/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/objects/timeline.ts @@ -10,6 +10,30 @@ export interface Timeline { query: string; } +export interface CompleteTimeline extends Timeline { + notes: string; + filter: TimelineFilter; +} + +export interface TimelineFilter { + field: string; + operator: string; + value?: string; +} + export interface TimelineWithId extends Timeline { id: string; } + +export const filter: TimelineFilter = { + field: 'host.name', + operator: 'exists', +}; + +export const timeline: CompleteTimeline = { + title: 'Security Timeline', + description: 'This is the best timeline', + query: 'host.name: * ', + notes: 'Yes, the best timeline', + filter, +}; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index bcb64fc947feb..94255a2af8976 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TimelineFilter } from '../objects/timeline'; + +export const ADD_NOTE_BUTTON = '[data-test-subj="add-note"]'; + +export const ADD_FILTER = '[data-test-subj="timeline"] [data-test-subj="addFilter"]'; + export const ATTACH_TIMELINE_TO_NEW_CASE_ICON = '[data-test-subj="attach-timeline-case"]'; export const ATTACH_TIMELINE_TO_EXISTING_CASE_ICON = @@ -15,14 +21,18 @@ export const CASE = (id: string) => { return `[data-test-subj="cases-table-row-${id}"]`; }; +export const CLOSE_NOTES_BTN = '[data-test-subj="notesModal"] .euiButtonIcon'; + export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; +export const COMBO_BOX = '.euiComboBoxOption__content'; + export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; export const DRAGGABLE_HEADER = '[data-test-subj="events-viewer-panel"] [data-test-subj="headers-group"] [data-test-subj="draggable-header"]'; -export const EXPORT_TIMELINE_ACTION = '[data-test-subj="export-timeline-action"]'; +export const FAVORITE_TIMELINE = '[data-test-subj="timeline-favorite-filled-star"]'; export const HEADER = '[data-test-subj="header"]'; @@ -34,6 +44,16 @@ export const ID_FIELD = '[data-test-subj="timeline"] [data-test-subj="field-name export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]'; +export const LOCKED_ICON = '[data-test-subj="timeline-date-picker-lock-button"]'; + +export const NOTES = '[data-test-subj="markdown-root"]'; + +export const NOTES_TEXT_AREA = '[data-test-subj="add-a-note"]'; + +export const NOTES_BUTTON = '[data-test-subj="timeline-notes-button-large"]'; + +export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]'; + export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; export const PIN_EVENT = '[data-test-subj="pin"]'; @@ -45,21 +65,17 @@ export const REMOVE_COLUMN = '[data-test-subj="remove-column"]'; export const RESET_FIELDS = '[data-test-subj="events-viewer-panel"] [data-test-subj="reset-fields"]'; +export const SAVE_FILTER_BTN = '[data-test-subj="saveFilter"]'; + export const SEARCH_OR_FILTER_CONTAINER = '[data-test-subj="timeline-search-or-filter-search-container"]'; export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; -export const TIMELINE = (id: string) => { - return `[data-test-subj="title-${id}"]`; -}; +export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]'; export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiProgress'; -export const TIMELINE_CHECKBOX = (id: string) => { - return `[data-test-subj="checkboxSelectRow-${id}"]`; -}; - export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; @@ -74,6 +90,17 @@ export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContain export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; +export const TIMELINE_FILTER = (filter: TimelineFilter) => { + return `[data-test-subj="filter filter-enabled filter-key-${filter.field} filter-value-${filter.value} filter-unpinned"]`; +}; + +export const TIMELINE_FILTER_FIELD = '[data-test-subj="filterFieldSuggestionList"]'; + +export const TIMELINE_FILTER_OPERATOR = '[data-test-subj="filterOperatorList"]'; + +export const TIMELINE_FILTER_VALUE = + '[data-test-subj="filterParamsComboBox phraseParamsComboxBox"]'; + export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="eui-flyout-header"]'; export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; @@ -89,8 +116,6 @@ export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; -export const TIMELINES_TABLE = '[data-test-subj="timelines-table"]'; - export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"]'; export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="toggle-field-@timestamp"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timelines.ts b/x-pack/plugins/security_solution/cypress/screens/timelines.ts new file mode 100644 index 0000000000000..e87e3c4f72ca5 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/timelines.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const BULK_ACTIONS = '[data-test-subj="utility-bar-action-button"]'; + +export const EXPORT_TIMELINE_ACTION = '[data-test-subj="export-timeline-action"]'; + +export const TIMELINE = (id: string) => { + return `[data-test-subj="title-${id}"]`; +}; + +export const TIMELINE_CHECKBOX = (id: string) => { + return `[data-test-subj="checkboxSelectRow-${id}"]`; +}; + +export const TIMELINES_FAVORITE = '[data-test-subj="favorite-starFilled-star"]'; + +export const TIMELINES_DESCRIPTION = '[data-test-subj="description"]'; + +export const TIMELINES_NOTES_COUNT = '[data-test-subj="notes-count"]'; + +export const TIMELINES_PINNED_EVENT_COUNT = '[data-test-subj="pinned-event-count"]'; + +export const TIMELINES_TABLE = '[data-test-subj="timelines-table"]'; + +export const TIMELINES_USERNAME = '[data-test-subj="username"]'; diff --git a/x-pack/plugins/security_solution/cypress/support/index.d.ts b/x-pack/plugins/security_solution/cypress/support/index.d.ts index f66aeff5d578d..f0b0b8c92c616 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.d.ts +++ b/x-pack/plugins/security_solution/cypress/support/index.d.ts @@ -6,6 +6,7 @@ declare namespace Cypress { interface Chainable { + promisify(): Promise; stubSecurityApi(dataFileName: string): Chainable; stubSearchStrategyApi(dataFileName: string): Chainable; attachFile(fileName: string, fileType?: string): Chainable; diff --git a/x-pack/plugins/security_solution/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/index.js index 244781e0ccd01..1328bea0e2918 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.js +++ b/x-pack/plugins/security_solution/cypress/support/index.js @@ -21,6 +21,7 @@ // Import commands.js using ES2015 syntax: import './commands'; +import 'cypress-promise/register'; Cypress.Cookies.defaults({ preserve: 'sid', diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 0daff52de7063..dc89a39d082bc 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -55,7 +55,7 @@ import { EQL_TYPE, EQL_QUERY_INPUT, } from '../screens/create_new_rule'; -import { TIMELINE } from '../screens/timeline'; +import { TIMELINE } from '../screens/timelines'; export const createAndActivateRule = () => { cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts index 47b73db8b96df..6b1f3699d333a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_main.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_main.ts @@ -6,14 +6,14 @@ import { MAIN_PAGE, TIMELINE_TOGGLE_BUTTON } from '../screens/security_main'; -export const openTimeline = () => { +export const openTimelineUsingToggle = () => { cy.get(TIMELINE_TOGGLE_BUTTON).click(); }; export const openTimelineIfClosed = () => { cy.get(MAIN_PAGE).then(($page) => { if ($page.find(TIMELINE_TOGGLE_BUTTON).length === 1) { - openTimeline(); + openTimelineUsingToggle(); } }); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index cd8b197fc4dec..438700bdfca80 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -4,36 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TimelineFilter } from '../objects/timeline'; + import { ALL_CASES_CREATE_NEW_CASE_TABLE_BTN } from '../screens/all_cases'; + import { - BULK_ACTIONS, + ADD_FILTER, + ADD_NOTE_BUTTON, + ATTACH_TIMELINE_TO_EXISTING_CASE_ICON, + ATTACH_TIMELINE_TO_NEW_CASE_ICON, + CASE, CLOSE_TIMELINE_BTN, + CLOSE_NOTES_BTN, + COMBO_BOX, CREATE_NEW_TIMELINE, - EXPORT_TIMELINE_ACTION, - TIMELINE_CHECKBOX, HEADER, ID_FIELD, ID_HEADER_FIELD, ID_TOGGLE_FIELD, + NOTES_BUTTON, + NOTES_TEXT_AREA, + OPEN_TIMELINE_ICON, PIN_EVENT, + REMOVE_COLUMN, + RESET_FIELDS, + SAVE_FILTER_BTN, SEARCH_OR_FILTER_CONTAINER, SERVER_SIDE_EVENT_COUNT, + STAR_ICON, TIMELINE_CHANGES_IN_PROGRESS, TIMELINE_DESCRIPTION, TIMELINE_FIELDS_BUTTON, + TIMELINE_FILTER_FIELD, + TIMELINE_FILTER_OPERATOR, + TIMELINE_FILTER_VALUE, TIMELINE_INSPECT_BUTTON, TIMELINE_SETTINGS_ICON, TIMELINE_TITLE, - TIMELINES_TABLE, TIMESTAMP_TOGGLE_FIELD, TOGGLE_TIMELINE_EXPAND_EVENT, - REMOVE_COLUMN, - RESET_FIELDS, - ATTACH_TIMELINE_TO_NEW_CASE_ICON, - OPEN_TIMELINE_ICON, - ATTACH_TIMELINE_TO_EXISTING_CASE_ICON, - CASE, } from '../screens/timeline'; +import { TIMELINES_TABLE } from '../screens/timelines'; import { drag, drop } from '../tasks/common'; @@ -49,6 +60,24 @@ export const addNameToTimeline = (name: string) => { cy.get(TIMELINE_TITLE).should('have.attr', 'value', name); }; +export const addNotesToTimeline = (notes: string) => { + cy.get(NOTES_BUTTON).click(); + cy.get(NOTES_TEXT_AREA).type(notes); + cy.get(ADD_NOTE_BUTTON).click(); +}; + +export const addFilter = (filter: TimelineFilter) => { + cy.get(ADD_FILTER).click(); + cy.get(TIMELINE_FILTER_FIELD).type(filter.field); + cy.get(COMBO_BOX).contains(filter.field).click(); + cy.get(TIMELINE_FILTER_OPERATOR).type(filter.operator); + cy.get(COMBO_BOX).contains(filter.operator).click(); + if (filter.operator !== 'exists') { + cy.get(TIMELINE_FILTER_VALUE).type(`${filter.value}{enter}`); + } + cy.get(SAVE_FILTER_BTN).click(); +}; + export const addNewCase = () => { cy.get(ALL_CASES_CREATE_NEW_CASE_TABLE_BTN).click(); }; @@ -71,6 +100,10 @@ export const checkIdToggleField = () => { }); }; +export const closeNotes = () => { + cy.get(CLOSE_NOTES_BTN).click(); +}; + export const closeTimeline = () => { cy.get(CLOSE_TIMELINE_BTN).click({ force: true }); }; @@ -89,10 +122,8 @@ export const expandFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true }); }; -export const exportTimeline = (timelineId: string) => { - cy.get(TIMELINE_CHECKBOX(timelineId)).click({ force: true }); - cy.get(BULK_ACTIONS).click({ force: true }); - cy.get(EXPORT_TIMELINE_ACTION).click(); +export const markAsFavorite = () => { + cy.get(STAR_ICON).click(); }; export const openTimelineFieldsBrowser = () => { @@ -160,11 +191,11 @@ export const selectCase = (caseId: string) => { cy.get(CASE(caseId)).click(); }; -export const waitForTimelinesPanelToBeLoaded = () => { - cy.get(TIMELINES_TABLE).should('exist'); -}; - export const waitForTimelineChanges = () => { cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('exist'); cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('not.exist'); }; + +export const waitForTimelinesPanelToBeLoaded = () => { + cy.get(TIMELINES_TABLE).should('exist'); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts new file mode 100644 index 0000000000000..1c5ce246a35b3 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + TIMELINE_CHECKBOX, + BULK_ACTIONS, + EXPORT_TIMELINE_ACTION, + TIMELINES_TABLE, + TIMELINE, +} from '../screens/timelines'; + +export const exportTimeline = (timelineId: string) => { + cy.get(TIMELINE_CHECKBOX(timelineId)).click({ force: true }); + cy.get(BULK_ACTIONS).click({ force: true }); + cy.get(EXPORT_TIMELINE_ACTION).click(); +}; + +export const openTimeline = (id: string) => { + cy.get(TIMELINE(id)).click(); +}; + +export const waitForTimelinesPanelToBeLoaded = () => { + cy.get(TIMELINES_TABLE).should('exist'); +}; diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index fd7941fb17cc5..6982c200a5afd 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -1,4 +1,4 @@ -{ + { "author": "Elastic", "name": "security_solution", "version": "8.0.0", diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 073cb46d3949a..f2eb5cf5b94f3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -429,5 +429,11 @@ describe('helpers', () => { expect(result.description).toEqual('Threshold'); }); + + it('returns a humanized description for a threat_match type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'threat_match'); + + expect(result.description).toEqual('Threat Match'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 0c866ae0bd926..4d46d4dc86846 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -391,6 +391,14 @@ export const buildRuleTypeDescription = (label: string, ruleType: Type): ListIte }, ]; } + case 'threat_match': { + return [ + { + title: label, + description: i18n.THREAT_MATCH_TYPE_DESCRIPTION, + }, + ]; + } default: return assertUnreachable(ruleType); } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx index 124ef9e648403..d714f04f519d4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx @@ -55,6 +55,13 @@ export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate( } ); +export const THREAT_MATCH_TYPE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.threatMatchRuleTypeDescription', + { + defaultMessage: 'Threat Match', + } +); + export const ML_JOB_STARTED = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStartedDescription', { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 2acb3e57c5a3b..65a5c6aca0050 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -94,6 +94,7 @@ export const filterRuleFieldsForType = (fields: T, type: T case 'threshold': const { anomalyThreshold, machineLearningJobId, ...thresholdRuleFields } = fields; return thresholdRuleFields; + case 'threat_match': case 'query': case 'saved_query': case 'eql': diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index dbfb0333e48ee..42fbe40d690ea 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -315,6 +315,7 @@ const getRuleSpecificRuleParamKeys = (ruleType: Type) => { return ['anomaly_threshold', 'machine_learning_job_id']; case 'threshold': return ['threshold', ...queryRuleParams]; + case 'threat_match': case 'query': case 'saved_query': case 'eql': diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index c0c9cdf227643..218cef36ed50a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -34,7 +34,7 @@ describe('TrustedAppsPage', () => { }); }); - test('rendering', () => { + test.skip('rendering', () => { expect(render()).toMatchSnapshot(); }); @@ -78,7 +78,7 @@ describe('TrustedAppsPage', () => { expect(history.location.search).toBe('?page_index=2&page_size=20&show=create'); }); - it('should display create form', async () => { + it.skip('should display create form', async () => { const { getByTestId } = await renderAndClickAddButton(); expect(getByTestId('addTrustedAppFlyout-createForm')).toMatchSnapshot(); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx deleted file mode 100644 index db4b514a6c748..0000000000000 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ /dev/null @@ -1,521 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable react/display-name */ - -import React, { memo } from 'react'; -import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; -import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; -import { htmlIdGenerator, ButtonColor } from '@elastic/eui'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { ResolverProcessType } from '../types'; -import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; - -type ResolverColorNames = - | 'descriptionText' - | 'full' - | 'graphControls' - | 'graphControlsBackground' - | 'resolverBackground' - | 'resolverEdge' - | 'resolverEdgeText' - | 'resolverBreadcrumbBackground' - | 'pillStroke'; - -type ColorMap = Record; -interface NodeStyleConfig { - backingFill: string; - cubeSymbol: string; - descriptionFill: string; - descriptionText: string; - isLabelFilled: boolean; - labelButtonFill: ButtonColor; - strokeColor: string; -} - -interface NodeStyleMap { - runningProcessCube: NodeStyleConfig; - runningTriggerCube: NodeStyleConfig; - terminatedProcessCube: NodeStyleConfig; - terminatedTriggerCube: NodeStyleConfig; -} - -const idGenerator = htmlIdGenerator(); - -/** - * Ids of paint servers to be referenced by fill and stroke attributes - */ -const PaintServerIds = { - runningProcessCube: idGenerator('psRunningProcessCube'), - runningTriggerCube: idGenerator('psRunningTriggerCube'), - terminatedProcessCube: idGenerator('psTerminatedProcessCube'), - terminatedTriggerCube: idGenerator('psTerminatedTriggerCube'), -}; - -/** - * PaintServers: Where color palettes, grandients, patterns and other similar concerns - * are exposed to the component - */ - -const PaintServers = memo(({ isDarkMode }: { isDarkMode: boolean }) => ( - <> - - - - - - - - - {isDarkMode ? ( - <> - - - - - - - - - - ) : ( - <> - - - - - - - - - - )} - -)); - -/** - * Ids of symbols to be linked by elements - */ -export const SymbolIds = { - processNodeLabel: idGenerator('nodeSymbol'), - runningProcessCube: idGenerator('runningCube'), - runningTriggerCube: idGenerator('runningTriggerCube'), - terminatedProcessCube: idGenerator('terminatedCube'), - terminatedTriggerCube: idGenerator('terminatedTriggerCube'), - processCubeActiveBacking: idGenerator('activeBacking'), -}; - -/** - * Defs entries that define shapes, masks and other spatial elements - */ -const SymbolsAndShapes = memo(({ isDarkMode }: { isDarkMode: boolean }) => ( - <> - - - - - {'Running Process'} - - - - - - - - - - - - - - {'resolver_dark process running'} - - - - - - - - - - - - - - {'Terminated Process'} - - - - - - - - - {'Terminated Trigger Process'} - {isDarkMode && ( - - )} - - {!isDarkMode && ( - - )} - - - - - - - {'resolver active backing'} - - - -)); - -/** - * This `` element is used to define the reusable assets for the Resolver - * It confers several advantages, including but not limited to: - * 1. Freedom of form for creative assets (beyond box-model constraints) - * 2. Separation of concerns between creative assets and more functional areas of the app - * 3. `` elements can be handled by compositor (faster) - */ -const SymbolDefinitionsComponent = memo(({ className }: { className?: string }) => { - const isDarkMode = useUiSetting('theme:darkMode'); - return ( - - - - - - - ); -}); - -export const SymbolDefinitions = styled(SymbolDefinitionsComponent)` - position: absolute; - left: 100%; - top: 100%; - width: 0; - height: 0; -`; - -const processTypeToCube: Record = { - processCreated: 'runningProcessCube', - processRan: 'runningProcessCube', - processTerminated: 'terminatedProcessCube', - unknownProcessEvent: 'runningProcessCube', - processCausedAlert: 'runningTriggerCube', - unknownEvent: 'runningProcessCube', -}; - -/** - * A hook to bring Resolver theming information into components. - */ -export const useResolverTheme = (): { - colorMap: ColorMap; - nodeAssets: NodeStyleMap; - cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessTrigger: boolean) => NodeStyleConfig; -} => { - const isDarkMode = useUiSetting('theme:darkMode'); - const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; - - const getThemedOption = (lightOption: string, darkOption: string): string => { - return isDarkMode ? darkOption : lightOption; - }; - - const colorMap = { - descriptionText: theme.euiTextColor, - full: theme.euiColorFullShade, - graphControls: theme.euiColorDarkestShade, - graphControlsBackground: theme.euiColorEmptyShade, - processBackingFill: `${theme.euiColorPrimary}${getThemedOption('0F', '1F')}`, // Add opacity 0F = 6% , 1F = 12% - resolverBackground: theme.euiColorEmptyShade, - resolverEdge: getThemedOption(theme.euiColorLightestShade, theme.euiColorLightShade), - resolverBreadcrumbBackground: theme.euiColorLightestShade, - resolverEdgeText: getThemedOption(theme.euiColorDarkShade, theme.euiColorFullShade), - triggerBackingFill: `${theme.euiColorDanger}${getThemedOption('0F', '1F')}`, - pillStroke: theme.euiColorLightShade, - }; - - const nodeAssets: NodeStyleMap = { - runningProcessCube: { - backingFill: colorMap.processBackingFill, - cubeSymbol: `#${SymbolIds.runningProcessCube}`, - descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningProcess', { - defaultMessage: 'Running Process', - }), - isLabelFilled: true, - labelButtonFill: 'primary', - strokeColor: theme.euiColorPrimary, - }, - runningTriggerCube: { - backingFill: colorMap.triggerBackingFill, - cubeSymbol: `#${SymbolIds.runningTriggerCube}`, - descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningTrigger', { - defaultMessage: 'Running Trigger', - }), - isLabelFilled: true, - labelButtonFill: 'danger', - strokeColor: theme.euiColorDanger, - }, - terminatedProcessCube: { - backingFill: colorMap.processBackingFill, - cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, - descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.terminatedProcess', - { - defaultMessage: 'Terminated Process', - } - ), - isLabelFilled: false, - labelButtonFill: 'primary', - strokeColor: theme.euiColorPrimary, - }, - terminatedTriggerCube: { - backingFill: colorMap.triggerBackingFill, - cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, - descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.terminatedTrigger', - { - defaultMessage: 'Terminated Trigger', - } - ), - isLabelFilled: false, - labelButtonFill: 'danger', - strokeColor: theme.euiColorDanger, - }, - }; - - function cubeAssetsForNode(isProcessTerminated: boolean, isProcessTrigger: boolean) { - if (isProcessTerminated) { - if (isProcessTrigger) { - return nodeAssets.terminatedTriggerCube; - } else { - return nodeAssets[processTypeToCube.processTerminated]; - } - } else if (isProcessTrigger) { - return nodeAssets[processTypeToCube.processCausedAlert]; - } else { - return nodeAssets[processTypeToCube.processRan]; - } - } - - return { colorMap, nodeAssets, cubeAssetsForNode }; -}; - -export const calculateResolverFontSize = ( - magFactorX: number, - minFontSize: number, - slopeOfFontScale: number -): number => { - const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0; - return minFontSize + fontSizeAdjustmentForScale; -}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 935e565be039e..dba1136193ee1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -276,48 +276,12 @@ describe('Resolver, when analyzing a tree that has two related events for the or ); expect(edgesThatTerminateUnderneathSecondChild).toHaveLength(1); }); - - it('should render a related events button', async () => { + it('should show exactly one option with the correct count', async () => { await expect( - simulator.map(() => ({ - relatedEventButtons: simulator.processNodeSubmenuButton(entityIDs.origin).length, - })) - ).toYieldEqualTo({ - relatedEventButtons: 1, - }); - }); - describe('when the related events button is clicked', () => { - beforeEach(async () => { - const button = await simulator.resolveWrapper(() => - simulator.processNodeSubmenuButton(entityIDs.origin) - ); - if (button) { - button.simulate('click', { button: 0 }); - } - }); - it('should open the submenu and display exactly one option with the correct count', async () => { - await expect( - simulator.map(() => - simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text()) - ) - ).toYieldEqualTo(['2 registry']); - }); - }); - describe('and when the related events button is clicked again', () => { - beforeEach(async () => { - const button = await simulator.resolveWrapper(() => - simulator.processNodeSubmenuButton(entityIDs.origin) - ); - if (button) { - button.simulate('click', { button: 0 }); - button.simulate('click', { button: 0 }); // The first click opened the menu, this second click closes it - } - }); - it('should close the submenu', async () => { - await expect( - simulator.map(() => simulator.testSubject('resolver:map:node-submenu-item').length) - ).toYieldEqualTo(0); - }); + simulator.map(() => + simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text()) + ) + ).toYieldEqualTo(['2 registry']); }); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index fcc363a1560d5..53b889004798f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -9,7 +9,8 @@ import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { applyMatrix3, distance, angle } from '../models/vector2'; import { Vector2, Matrix3, EdgeLineMetadata } from '../types'; -import { useResolverTheme, calculateResolverFontSize } from './assets'; +import { fontSize } from './font_size'; +import { useColors } from './use_colors'; interface StyledEdgeLine { readonly resolverEdgeColor: string; @@ -19,7 +20,7 @@ interface StyledEdgeLine { const StyledEdgeLine = styled.div` position: absolute; height: ${(props) => { - return `${calculateResolverFontSize(props.magFactorX, 12, 8.5)}px`; + return `${fontSize(props.magFactorX, 12, 8.5)}px`; }}; background-color: ${(props) => props.resolverEdgeColor}; `; @@ -87,8 +88,8 @@ const EdgeLineComponent = React.memo( */ const screenStart = applyMatrix3(startPosition, projectionMatrix); const screenEnd = applyMatrix3(endPosition, projectionMatrix); - const [magFactorX] = projectionMatrix; - const { colorMap } = useResolverTheme(); + const [xScale] = projectionMatrix; + const colorMap = useColors(); const elapsedTime = edgeLineMetadata?.elapsedTime; /** @@ -96,7 +97,7 @@ const EdgeLineComponent = React.memo( * should be the same as the distance between the start and end points. */ const length = distance(screenStart, screenEnd); - const scaledTypeSize = calculateResolverFontSize(magFactorX, 10, 7.5); + const scaledTypeSize = fontSize(xScale, 10, 7.5); const style = { left: `${screenStart[0]}px`, @@ -120,8 +121,8 @@ const EdgeLineComponent = React.memo( /** * Calculates a fractional offset from 0 -> 5% as magFactorX decreases from 1 to a min of .5 */ - if (magFactorX < 1) { - const fractionalOffset = (1 / magFactorX) * ((1 - magFactorX) * 10); + if (xScale < 1) { + const fractionalOffset = (1 / xScale) * ((1 - xScale) * 10); elapsedTimeLeftPosPct += fractionalOffset; } @@ -130,7 +131,7 @@ const EdgeLineComponent = React.memo( className={className} style={style} resolverEdgeColor={colorMap.resolverEdge} - magFactorX={magFactorX} + magFactorX={xScale} data-test-subj="resolver:graph:edgeline" > {elapsedTime && ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/font_size.ts b/x-pack/plugins/security_solution/public/resolver/view/font_size.ts new file mode 100644 index 0000000000000..d0340160eb539 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/font_size.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Return a font-size based on a scale, minimum size, and a coefficient. + */ +export function fontSize(scale: number, minimum: number, slope: number): number { + return minimum + (scale > 1 ? slope * (scale - 1) : 0); +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 610deef07775b..75aecf6747cca 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -13,8 +13,8 @@ import { useSelector, useDispatch } from 'react-redux'; import { SideEffectContext } from './side_effect_context'; import { Vector2 } from '../types'; import * as selectors from '../store/selectors'; -import { useResolverTheme } from './assets'; import { ResolverAction } from '../store/actions'; +import { useColors } from './use_colors'; interface StyledGraphControls { graphControlsBackground: string; @@ -66,7 +66,7 @@ const GraphControlsComponent = React.memo( const dispatch: (action: ResolverAction) => unknown = useDispatch(); const scalingFactor = useSelector(selectors.scalingFactor); const { timestamp } = useContext(SideEffectContext); - const { colorMap } = useResolverTheme(); + const colorMap = useColors(); const handleZoomAmountChange = useCallback( (event: React.ChangeEvent | React.MouseEvent) => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx index deddd17198229..4e9d64f5a76a4 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx @@ -11,11 +11,12 @@ import { i18n } from '@kbn/i18n'; /* eslint-disable react/display-name */ import React, { memo } from 'react'; -import { useResolverTheme, SymbolIds } from '../assets'; interface StyledSVGCube { readonly isOrigin?: boolean; } +import { useCubeAssets } from '../use_cube_assets'; +import { useSymbolIDs } from '../use_symbol_ids'; /** * Icon representing a process node. @@ -34,8 +35,8 @@ export const CubeForProcess = memo(function ({ isOrigin?: boolean; className?: string; }) { - const { cubeAssetsForNode } = useResolverTheme(); - const { cubeSymbol, strokeColor } = cubeAssetsForNode(!running, false); + const { cubeSymbol, strokeColor } = useCubeAssets(!running, false); + const { processCubeActiveBacking } = useSymbolIDs(); return ( {isOrigin && ( { - if (!processEvent) { - return { descriptionText: '' }; - } - return cubeAssetsForNode(isProcessTerminated, false); - }, [processEvent, cubeAssetsForNode, isProcessTerminated]); + const { descriptionText } = useCubeAssets(isProcessTerminated, false); const nodeDetailHref = useSelector((state: ResolverState) => selectors.relativeHref(state)({ diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx index fd564cde9d15c..6113cea4c4edc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx @@ -26,7 +26,7 @@ import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { LimitWarning } from '../limit_warnings'; import { ResolverState } from '../../types'; import { useNavigateOrReplace } from '../use_navigate_or_replace'; -import { useResolverTheme } from '../assets'; +import { useColors } from '../use_colors'; const StyledLimitWarning = styled(LimitWarning)` flex-flow: row wrap; @@ -208,9 +208,7 @@ function NodeDetailLink({ name, item }: { name: string; item: ProcessTableView } const isTerminated = useSelector((state: ResolverState) => entityID === undefined ? false : selectors.isProcessTerminated(state)(entityID) ); - const { - colorMap: { descriptionText }, - } = useResolverTheme(); + const { descriptionText } = useColors(); return ( {name === '' ? ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 19f0aa3fe1d67..a7d76277c6ab1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import { i18n } from '@kbn/i18n'; import { EuiBreadcrumbs, EuiCode, EuiBetaBadge } from '@elastic/eui'; import styled from 'styled-components'; import React, { memo } from 'react'; -import { useResolverTheme } from '../assets'; +import { useColors } from '../use_colors'; /** * A bold version of EuiCode to display certain titles with @@ -63,7 +65,7 @@ export const GeneratedText = React.memo(function ({ children }) { valueSplitByWordBoundaries[0], ...valueSplitByWordBoundaries .splice(1) - .reduce(function (generatedTextMemo: Array, value, index) { + .reduce(function (generatedTextMemo: Array, value) { return [...generatedTextMemo, value, ]; }, []), ]; @@ -73,7 +75,6 @@ export const GeneratedText = React.memo(function ({ children }) { }); } }); -GeneratedText.displayName = 'GeneratedText'; /** * A component to keep time representations in blocks so they don't wrap @@ -93,9 +94,7 @@ export const StyledBreadcrumbs = memo(function StyledBreadcrumbs({ }: { breadcrumbs: Breadcrumbs; }) { - const { - colorMap: { resolverBreadcrumbBackground, resolverEdgeText }, - } = useResolverTheme(); + const { resolverBreadcrumbBackground, resolverEdgeText } = useColors(); return ( <> diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 3edfe36087e68..65ec395080f86 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -12,12 +12,15 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { NodeSubMenu } from './submenu'; import { applyMatrix3 } from '../models/vector2'; import { Vector2, Matrix3, ResolverState } from '../types'; -import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets'; import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../common/endpoint/models/event'; import * as selectors from '../store/selectors'; import { useNavigateOrReplace } from './use_navigate_or_replace'; +import { fontSize } from './font_size'; +import { useCubeAssets } from './use_cube_assets'; +import { useSymbolIDs } from './use_symbol_ids'; +import { useColors } from './use_colors'; interface StyledActionsContainer { readonly color: string; @@ -108,6 +111,8 @@ const UnstyledProcessEventDot = React.memo( // This should be unique to each instance of Resolver const htmlIDPrefix = `resolver:${resolverComponentInstanceID}`; + const symbolIDs = useSymbolIDs(); + /** * Convert the position, which is in 'world' coordinates, to screen coordinates. */ @@ -191,7 +196,7 @@ const UnstyledProcessEventDot = React.memo( * 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this. * 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise */ - const scaledTypeSize = calculateResolverFontSize(xScale, 18.75, 12.5); + const scaledTypeSize = fontSize(xScale, 18.75, 12.5); const markerBaseSize = 15; const markerSize = markerBaseSize; @@ -212,7 +217,7 @@ const UnstyledProcessEventDot = React.memo( }) | null; } = React.createRef(); - const { colorMap, cubeAssetsForNode } = useResolverTheme(); + const colorMap = useColors(); const { backingFill, cubeSymbol, @@ -220,7 +225,7 @@ const UnstyledProcessEventDot = React.memo( isLabelFilled, labelButtonFill, strokeColor, - } = cubeAssetsForNode( + } = useCubeAssets( isProcessTerminated, /** * There is no definition for 'trigger process' yet. return false. @@ -252,13 +257,6 @@ const UnstyledProcessEventDot = React.memo( }); }, [dispatch, nodeID]); - const handleRelatedEventRequest = useCallback(() => { - dispatch({ - type: 'userRequestedRelatedEventData', - payload: nodeID, - }); - }, [dispatch, nodeID]); - const handleClick = useCallback( (clickEvent) => { if (animationTarget.current?.beginElement) { @@ -323,7 +321,7 @@ const UnstyledProcessEventDot = React.memo( > {isOrigin && ( {grandTotal !== null && grandTotal > 0 && ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index f96d89b893ceb..13dcfcabe50cb 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -16,13 +16,14 @@ import { EdgeLine } from './edge_line'; import { GraphControls } from './graph_controls'; import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; -import { SymbolDefinitions, useResolverTheme } from './assets'; +import { SymbolDefinitions } from './symbol_definitions'; import { useStateSyncingActions } from './use_state_syncing_actions'; import { StyledMapContainer, GraphContainer } from './styles'; import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; import { SideEffectContext } from './side_effect_context'; import { ResolverProps, ResolverState } from '../types'; import { PanelRouter } from './panels'; +import { useColors } from './use_colors'; /** * The highest level connected Resolver component. Needs a `Provider` in its ancestry to work. @@ -73,7 +74,7 @@ export const ResolverWithoutProviders = React.memo( const isLoading = useSelector(selectors.isTreeLoading); const hasError = useSelector(selectors.hadErrorLoadingTree); const activeDescendantId = useSelector(selectors.ariaActiveDescendant); - const { colorMap } = useResolverTheme(); + const colorMap = useColors(); return ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index dda90df0fff93..d40aa0b26a94b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -4,16 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react/display-name */ - import { i18n } from '@kbn/i18n'; -import React, { useState, useCallback, useRef, useLayoutEffect, useMemo } from 'react'; -import { EuiI18nNumber, EuiButton, EuiPopover, ButtonColor } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { EuiI18nNumber } from '@elastic/eui'; import styled from 'styled-components'; import { ResolverNodeStats } from '../../../common/endpoint/types'; import { useRelatedEventByCategoryNavigation } from './use_related_event_by_category_navigation'; -import { Matrix3 } from '../types'; -import { useResolverTheme } from './assets'; +import { useColors } from './use_colors'; /** * i18n-translated titles for submenus and identifiers for display of states: @@ -45,53 +42,6 @@ interface ResolverSubmenuOption { export type ResolverSubmenuOptionList = ResolverSubmenuOption[] | string; -const StyledActionButton = styled(EuiButton)` - &.euiButton--small { - height: fit-content; - line-height: 1; - padding: 0.25em; - font-size: 0.85rem; - } -`; - -/** - * This will be the "host button" that displays the "total number of related events" and opens - * the sumbmenu (with counts by category) when clicked. - */ -const SubButton = React.memo( - ({ - hasMenu, - menuIsOpen, - action, - count, - nodeID, - }: { - hasMenu: boolean; - menuIsOpen?: boolean; - action: (evt: React.MouseEvent) => void; - count?: number; - nodeID: string; - }) => { - const iconType = menuIsOpen === true ? 'arrowUp' : 'arrowDown'; - return ( - - {count ? : ''} {subMenuAssets.relatedEvents.title} - - ); - } -); - /** * A Submenu to be displayed in one of two forms: * 1) Provided a collection of `optionsWithActions`: it will call `menuAction` then - if and when menuData becomes available - display each item with an optional prefix and call the supplied action for the options when that option is clicked. @@ -99,53 +49,20 @@ const SubButton = React.memo( */ const NodeSubMenuComponents = React.memo( ({ - count, - buttonBorderColor, - menuAction, className, - projectionMatrix, nodeID, relatedEventStats, }: { className?: string; - menuAction?: () => unknown; - buttonBorderColor: ButtonColor; // eslint-disable-next-line react/no-unused-prop-types buttonFill: string; - count?: number; /** * Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself. */ - projectionMatrix: Matrix3; nodeID: string; relatedEventStats: ResolverNodeStats | undefined; }) => { - // keep a ref to the popover so we can call its reposition method - const popoverRef = useRef(null); - - const [menuIsOpen, setMenuOpen] = useState(false); - const handleMenuOpenClick = useCallback( - (clickEvent: React.MouseEvent) => { - // stopping propagation/default to prevent other node animations from triggering - clickEvent.preventDefault(); - clickEvent.stopPropagation(); - setMenuOpen(!menuIsOpen); - }, - [menuIsOpen] - ); - const handleMenuActionClick = useCallback( - (clickEvent: React.MouseEvent) => { - // stopping propagation/default to prevent other node animations from triggering - clickEvent.preventDefault(); - clickEvent.stopPropagation(); - if (typeof menuAction === 'function') menuAction(); - setMenuOpen(true); - }, - [menuAction] - ); - // The last projection matrix that was used to position the popover - const projectionMatrixAtLastRender = useRef(); const relatedEventCallbacks = useRelatedEventByCategoryNavigation({ nodeID, categories: relatedEventStats?.events?.byCategory, @@ -164,91 +81,39 @@ const NodeSubMenuComponents = React.memo( } }, [relatedEventStats, relatedEventCallbacks]); - useLayoutEffect(() => { - if ( - /** - * If there is a popover component reference, - * and this isn't the first render, - * and the projectionMatrix has changed since last render, - * then force the popover to reposition itself. - */ - popoverRef.current && - projectionMatrixAtLastRender.current && - projectionMatrixAtLastRender.current !== projectionMatrix - ) { - popoverRef.current.positionPopoverFixed(); - } - - // no matter what, keep track of the last project matrix that was used to size the popover - projectionMatrixAtLastRender.current = projectionMatrix; - }, [projectionMatrixAtLastRender, projectionMatrix]); - const { - colorMap: { pillStroke: pillBorderStroke, resolverBackground: pillFill }, - } = useResolverTheme(); + const { pillStroke: pillBorderStroke, resolverBackground: pillFill } = useColors(); const listStylesFromTheme = useMemo(() => { return { border: `1.5px solid ${pillBorderStroke}`, backgroundColor: pillFill, }; }, [pillBorderStroke, pillFill]); - if (relatedEventStats === undefined) { - /** - * When called with a `menuAction` - * Render without dropdown and call the supplied action when host button is clicked - */ - return ( -
- - {subMenuAssets.relatedEvents.title} - -
- ); - } if (relatedEventOptions === undefined) { return null; } return ( - <> - - {menuIsOpen ? ( -
    - {relatedEventOptions - .sort((opta, optb) => { - return opta.optionTitle.localeCompare(optb.optionTitle); - }) - .map((opt) => { - return ( -
  • - -
  • - ); - })} -
- ) : null} - +
    + {relatedEventOptions + .sort((opta, optb) => { + return opta.optionTitle.localeCompare(optb.optionTitle); + }) + .map((opt) => { + return ( +
  • + +
  • + ); + })} +
); } ); @@ -266,7 +131,7 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)` flex-flow: row wrap; background: transparent; position: absolute; - top: 6.5em; + top: 4.5em; contain: content; width: 12em; z-index: 2; @@ -301,17 +166,4 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)` &.options .item button:active { transform: scale(0.95); } - - & .euiButton { - background-color: ${(props) => props.buttonFill}; - border-color: ${(props) => props.buttonBorderColor}; - border-style: solid; - border-width: 1px; - - &:hover, - &:active, - &:focus { - background-color: ${(props) => props.buttonFill}; - } - } `; diff --git a/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx b/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx new file mode 100644 index 0000000000000..edf551c6cbeb9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx @@ -0,0 +1,354 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable react/display-name */ + +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; +import { useSymbolIDs } from './use_symbol_ids'; +import { usePaintServerIDs } from './use_paint_server_ids'; + +/** + * PaintServers: Where color palettes, gradients, patterns and other similar concerns + * are exposed to the component + */ +const PaintServers = memo(({ isDarkMode }: { isDarkMode: boolean }) => { + const paintServerIDs = usePaintServerIDs(); + return ( + <> + + + + + + + + + {isDarkMode ? ( + <> + + + + + + + + + + ) : ( + <> + + + + + + + + + + )} + + ); +}); + +/** + * Defs entries that define shapes, masks and other spatial elements + */ +const SymbolsAndShapes = memo(({ isDarkMode }: { isDarkMode: boolean }) => { + const symbolIDs = useSymbolIDs(); + const paintServerIDs = usePaintServerIDs(); + return ( + <> + + + + + {'Running Process'} + + + + + + + + + + + + + + {'resolver_dark process running'} + + + + + + + + + + + + + + {'Terminated Process'} + + + + + + + + + {'Terminated Trigger Process'} + {isDarkMode && ( + + )} + + {!isDarkMode && ( + + )} + + + + + + + {'resolver active backing'} + + + + ); +}); + +/** + * This `` element is used to define the reusable assets for the Resolver + * It confers several advantages, including but not limited to: + * 1. Freedom of form for creative assets (beyond box-model constraints) + * 2. Separation of concerns between creative assets and more functional areas of the app + * 3. `` elements can be handled by compositor (faster) + */ +export const SymbolDefinitions = memo(() => { + const isDarkMode = useUiSetting('theme:darkMode'); + return ( + + + + + + + ); +}); + +const HiddenSVG = styled('svg')` + position: absolute; + left: 100%; + top: 100%; + width: 0; + height: 0; +`; diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts new file mode 100644 index 0000000000000..8072266f1e8c8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; +import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; +import { useMemo } from 'react'; +import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; + +type ResolverColorNames = + | 'descriptionText' + | 'full' + | 'graphControls' + | 'graphControlsBackground' + | 'resolverBackground' + | 'resolverEdge' + | 'resolverEdgeText' + | 'resolverBreadcrumbBackground' + | 'pillStroke' + | 'triggerBackingFill' + | 'processBackingFill'; +type ColorMap = Record; + +/** + * Get access to Kibana-theme based colors. + */ +export function useColors(): ColorMap { + const isDarkMode = useUiSetting('theme:darkMode'); + const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; + return useMemo(() => { + return { + descriptionText: theme.euiTextColor, + full: theme.euiColorFullShade, + graphControls: theme.euiColorDarkestShade, + graphControlsBackground: theme.euiColorEmptyShade, + processBackingFill: `${theme.euiColorPrimary}${isDarkMode ? '1F' : '0F'}`, // Add opacity 0F = 6% , 1F = 12% + resolverBackground: theme.euiColorEmptyShade, + resolverEdge: isDarkMode ? theme.euiColorLightShade : theme.euiColorLightestShade, + resolverBreadcrumbBackground: theme.euiColorLightestShade, + resolverEdgeText: isDarkMode ? theme.euiColorFullShade : theme.euiColorDarkShade, + triggerBackingFill: `${theme.euiColorDanger}${isDarkMode ? '1F' : '0F'}`, + pillStroke: theme.euiColorLightShade, + }; + }, [isDarkMode, theme]); +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts new file mode 100644 index 0000000000000..c743ebc43f2be --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { ButtonColor } from '@elastic/eui'; +import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; +import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; +import { useMemo } from 'react'; +import { ResolverProcessType } from '../types'; +import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; +import { useSymbolIDs } from './use_symbol_ids'; +import { useColors } from './use_colors'; + +/** + * Provides colors and HTML IDs used to render the 'cube' graphic that accompanies nodes. + */ +export function useCubeAssets( + isProcessTerminated: boolean, + isProcessTrigger: boolean +): NodeStyleConfig { + const SymbolIds = useSymbolIDs(); + const isDarkMode = useUiSetting('theme:darkMode'); + const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; + const colorMap = useColors(); + + const nodeAssets: NodeStyleMap = useMemo( + () => ({ + runningProcessCube: { + backingFill: colorMap.processBackingFill, + cubeSymbol: `#${SymbolIds.runningProcessCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningProcess', { + defaultMessage: 'Running Process', + }), + isLabelFilled: true, + labelButtonFill: 'primary', + strokeColor: theme.euiColorPrimary, + }, + runningTriggerCube: { + backingFill: colorMap.triggerBackingFill, + cubeSymbol: `#${SymbolIds.runningTriggerCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningTrigger', { + defaultMessage: 'Running Trigger', + }), + isLabelFilled: true, + labelButtonFill: 'danger', + strokeColor: theme.euiColorDanger, + }, + terminatedProcessCube: { + backingFill: colorMap.processBackingFill, + cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.terminatedProcess', + { + defaultMessage: 'Terminated Process', + } + ), + isLabelFilled: false, + labelButtonFill: 'primary', + strokeColor: theme.euiColorPrimary, + }, + terminatedTriggerCube: { + backingFill: colorMap.triggerBackingFill, + cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.terminatedTrigger', + { + defaultMessage: 'Terminated Trigger', + } + ), + isLabelFilled: false, + labelButtonFill: 'danger', + strokeColor: theme.euiColorDanger, + }, + }), + [SymbolIds, colorMap, theme] + ); + + if (isProcessTerminated) { + if (isProcessTrigger) { + return nodeAssets.terminatedTriggerCube; + } else { + return nodeAssets[processTypeToCube.processTerminated]; + } + } else if (isProcessTrigger) { + return nodeAssets[processTypeToCube.processCausedAlert]; + } else { + return nodeAssets[processTypeToCube.processRan]; + } +} + +const processTypeToCube: Record = { + processCreated: 'runningProcessCube', + processRan: 'runningProcessCube', + processTerminated: 'terminatedProcessCube', + unknownProcessEvent: 'runningProcessCube', + processCausedAlert: 'runningTriggerCube', + unknownEvent: 'runningProcessCube', +}; +interface NodeStyleMap { + runningProcessCube: NodeStyleConfig; + runningTriggerCube: NodeStyleConfig; + terminatedProcessCube: NodeStyleConfig; + terminatedTriggerCube: NodeStyleConfig; +} +interface NodeStyleConfig { + backingFill: string; + cubeSymbol: string; + descriptionFill: string; + descriptionText: string; + isLabelFilled: boolean; + labelButtonFill: ButtonColor; + strokeColor: string; +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts b/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts new file mode 100644 index 0000000000000..0336a29bb0721 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; + +import { useSelector } from 'react-redux'; + +import * as selectors from '../store/selectors'; + +/** + * Access the HTML IDs for this Resolver's reusable SVG 'paint servers'. + * In the future these IDs may come from an outside provider (and may be shared by multiple Resolver instances.) + */ +export function usePaintServerIDs() { + const resolverComponentInstanceID = useSelector(selectors.resolverComponentInstanceID); + return useMemo(() => { + const prefix = `${resolverComponentInstanceID}-symbols`; + return { + runningProcessCube: `${prefix}-psRunningProcessCube`, + runningTriggerCube: `${prefix}-psRunningTriggerCube`, + terminatedProcessCube: `${prefix}-psTerminatedProcessCube`, + terminatedTriggerCube: `${prefix}-psTerminatedTriggerCube`, + }; + }, [resolverComponentInstanceID]); +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts b/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts new file mode 100644 index 0000000000000..0e1fd5737a3ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; + +import { useSelector } from 'react-redux'; + +import * as selectors from '../store/selectors'; + +/** + * Access the HTML IDs for this Resolver's reusable SVG symbols. + * In the future these IDs may come from an outside provider (and may be shared by multiple Resolver instances.) + */ +export function useSymbolIDs() { + const resolverComponentInstanceID = useSelector(selectors.resolverComponentInstanceID); + return useMemo(() => { + const prefix = `${resolverComponentInstanceID}-symbols`; + return { + processNodeLabel: `${prefix}-nodeSymbol`, + runningProcessCube: `${prefix}-runningCube`, + runningTriggerCube: `${prefix}-runningTriggerCube`, + terminatedProcessCube: `${prefix}-terminatedCube`, + terminatedTriggerCube: `${prefix}-terminatedTriggerCube`, + processCubeActiveBacking: `${prefix}-activeBacking`, + }; + }, [resolverComponentInstanceID]); +} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx index 71cf81c00dc09..1bfeb482873c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx @@ -146,7 +146,7 @@ const AddDataProviderPopoverComponent: React.FC = ( = ( {`+ ${ADD_FIELD_LABEL}`} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 4ab05af5dd6d4..0f6535015c799 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -376,7 +376,11 @@ const NotesButtonComponent = React.memo( )} {size === 'l' && showNotes ? ( - + { const exitIfTimesToRunIsInvalid = (timesToRun) => { if (!timesToRun > 0) { console.error( - '\nERROR: You must specify a valid number of times to run the SIEM Cypress tests.' + '\nERROR: You must specify a valid number of times to run the Security Solution Cypress tests.' ); showUsage(); process.exit(1); @@ -44,7 +44,7 @@ const spawnChild = async () => { const child = spawn('node', [ 'scripts/functional_tests', '--config', - 'x-pack/test/security_solution_cypress/config.ts', + 'x-pack/test/security_solution_cypress/cli_config.ts', ]); for await (const chunk of child.stdout) { console.log(chunk.toString()); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 29c56e8ed80b1..fb01f92255516 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -396,6 +396,10 @@ export const getResult = (): RuleAlertType => ({ ], threshold: undefined, timestampOverride: undefined, + threatFilters: undefined, + threatMapping: undefined, + threatIndex: undefined, + threatQuery: undefined, references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', version: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 959bf3186f136..dd887233c36a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -91,6 +91,10 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => severity_mapping: severityMapping, tags, threat, + threat_filters: threatFilters, + threat_index: threatIndex, + threat_mapping: threatMapping, + threat_query: threatQuery, threshold, throttle, timestamp_override: timestampOverride, @@ -176,6 +180,10 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => to, type, threat, + threatFilters, + threatMapping, + threatQuery, + threatIndex, threshold, timestampOverride, references, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 701e5b5e706ed..26ab89ad8ea7c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -78,6 +78,10 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void tags, threat, threshold, + threat_filters: threatFilters, + threat_index: threatIndex, + threat_query: threatQuery, + threat_mapping: threatMapping, throttle, timestamp_override: timestampOverride, to, @@ -162,6 +166,10 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void type, threat, threshold, + threatFilters, + threatIndex, + threatQuery, + threatMapping, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 0f44b50d4bc74..0f5d0304f5ca0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -158,6 +158,10 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP severity_mapping: severityMapping, tags, threat, + threat_filters: threatFilters, + threat_index: threatIndex, + threat_query: threatQuery, + threat_mapping: threatMapping, threshold, timestamp_override: timestampOverride, to, @@ -217,7 +221,11 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP to, type, threat, + threatFilters, + threatIndex, + threatQuery, threshold, + threatMapping, timestampOverride, references, note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index 11f74c264ae0c..2159245f2f735 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -19,7 +19,7 @@ import { } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; -import { RuleTypeParams } from '../../types'; +import { PartialFilter, RuleTypeParams } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; import { getOutputRuleAlertForRest } from '../__mocks__/utils'; import { createPromiseFromStreams } from '../../../../../../../../src/core/server/utils'; @@ -30,6 +30,7 @@ import { RuleAlertType } from '../../rules/types'; import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; @@ -122,6 +123,55 @@ describe('utils', () => { ); }); + test('transforms threat_matching fields', () => { + const threatRule = getResult(); + const threatFilters: PartialFilter[] = [ + { + query: { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + }, + ]; + const threatMapping: ThreatMapping = [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ]; + threatRule.params.threatIndex = 'index-123'; + threatRule.params.threatFilters = threatFilters; + threatRule.params.threatMapping = threatMapping; + threatRule.params.threatQuery = '*:*'; + + const rule = transformAlertToRule(threatRule); + expect(rule).toEqual( + expect.objectContaining({ + threat_index: 'index-123', + threat_filters: threatFilters, + threat_mapping: threatMapping, + threat_query: '*:*', + }) + ); + }); + // This has to stay here until we do data migration of saved objects and lists is removed from: // signal_params_schema.ts test('does not leak a lists structure in the transform which would cause validation issues', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index ee83ea91578c5..556ea209152e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -145,6 +145,10 @@ export const transformAlertToRule = ( type: alert.params.type, threat: alert.params.threat ?? [], threshold: alert.params.threshold, + threat_filters: alert.params.threatFilters, + threat_index: alert.params.threatIndex, + threat_query: alert.params.threatQuery, + threat_mapping: alert.params.threatMapping, throttle: ruleActions?.ruleThrottle || 'no_actions', timestamp_override: alert.params.timestampOverride, note: alert.params.note, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts index 1117f34b6f8c5..95067e57868d1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts @@ -39,6 +39,10 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threatFilters: undefined, + threatMapping: undefined, + threatQuery: undefined, + threatIndex: undefined, threshold: undefined, timestampOverride: undefined, to: 'now', @@ -82,6 +86,10 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ severityMapping: [], tags: [], threat: [], + threatFilters: undefined, + threatIndex: undefined, + threatMapping: undefined, + threatQuery: undefined, threshold: undefined, timestampOverride: undefined, to: 'now', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index 0c67d9ca77146..9ed94cd7bff2e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -42,6 +42,10 @@ export const createRules = async ({ severityMapping, tags, threat, + threatFilters, + threatIndex, + threatQuery, + threatMapping, threshold, timestampOverride, to, @@ -86,6 +90,10 @@ export const createRules = async ({ severityMapping, threat, threshold, + threatFilters, + threatIndex, + threatQuery, + threatMapping, timestampOverride, to, type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts index 3af0c3f55b485..59e14dcffc3c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -47,6 +47,10 @@ export const installPrepackagedRules = ( to, type, threat, + threat_filters: threatFilters, + threat_mapping: threatMapping, + threat_query: threatQuery, + threat_index: threatIndex, threshold, timestamp_override: timestampOverride, references, @@ -93,6 +97,10 @@ export const installPrepackagedRules = ( to, type, threat, + threatFilters, + threatMapping, + threatQuery, + threatIndex, threshold, timestampOverride, references, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index b845990fd94ef..6b851351f27f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -85,6 +85,13 @@ import { BuildingBlockTypeOrUndefined, RuleNameOverrideOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; +import { + ThreatIndexOrUndefined, + ThreatQueryOrUndefined, + ThreatMappingOrUndefined, + ThreatFiltersOrUndefined, +} from '../../../../common/detection_engine/schemas/types/threat_mapping'; + import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; @@ -206,6 +213,10 @@ export interface CreateRulesOptions { tags: Tags; threat: Threat; threshold: ThresholdOrUndefined; + threatFilters: ThreatFiltersOrUndefined; + threatIndex: ThreatIndexOrUndefined; + threatQuery: ThreatQueryOrUndefined; + threatMapping: ThreatMappingOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; type: Type; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh new file mode 100755 index 0000000000000..23c1914387c44 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + + +# Adds port mock data to a threat list for testing. +# Example: ./create_threat_data.sh +# Example: ./create_threat_data.sh 1000 2000 + +START=${1:-1} +END=${2:-1000} + +for (( i=$START; i<=$END; i++ )) +do { +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${ELASTICSEARCH_URL}/mock-threat-list/_doc/$i \ + --data " +{ + \"@timestamp\": \"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\", + \"source\": { \"ip\": \"127.0.0.1\", \"port\": \"${i}\" }, + \"destination\": { \"ip\": \"127.0.0.1\", \"port\": \"${i}\" } +}" +} > /dev/null +done diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_mapping.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_mapping.sh new file mode 100755 index 0000000000000..b0ec2973b2dd9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_mapping.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Add a small partial ECS based mapping of just source.ip, source.port, destination.ip, destination.port +# dnd then adds a large volume of threat lists to it + +# Example: .create_threat_mapping.sh + +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${ELASTICSEARCH_URL}/mock-threat-list \ + --data ' +{ + "mappings": { + "dynamic": "strict", + "properties": { + "@timestamp": { + "type": "date" + }, + "source": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "destination": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "host": { + "properties": { + "name": { + "type": "keyword" + }, + "ip" : { + "type" : "ip" + } + } + } + } + } +}' | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_threat_list.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_threat_list.sh new file mode 100755 index 0000000000000..85eac94a2991f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_threat_list.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Deletes a mock threat list +# Example: ./delete_threat_list.sh + +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${ELASTICSEARCH_URL}/mock-threat-list \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json new file mode 100644 index 0000000000000..c914e568048a1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json @@ -0,0 +1,60 @@ +{ + "name": "Query with a threat mapping", + "description": "Query with a threat mapping", + "rule_id": "threat-mapping", + "risk_score": 1, + "severity": "high", + "type": "threat_match", + "query": "*:*", + "tags": ["tag_1", "tag_2"], + "threat_index": "mock-threat-list", + "threat_query": "*:*", + "threat_mapping": [ + { + "entries": [ + { + "field": "host.name", + "type": "mapping", + "value": "host.name" + }, + { + "field": "host.ip", + "type": "mapping", + "value": "host.ip" + } + ] + }, + { + "entries": [ + { + "field": "destination.ip", + "type": "mapping", + "value": "destination.ip" + }, + { + "field": "destination.port", + "type": "mapping", + "value": "destination.port" + } + ] + }, + { + "entries": [ + { + "field": "source.port", + "type": "mapping", + "value": "source.port" + } + ] + }, + { + "entries": [ + { + "field": "source.ip", + "type": "mapping", + "value": "source.ip" + } + ] + } + ] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 9d3eb29be08dd..bbdb8ea0a36ed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -47,6 +47,10 @@ export const sampleRuleAlertParams = ( filters: undefined, savedId: undefined, threshold: undefined, + threatFilters: undefined, + threatQuery: undefined, + threatMapping: undefined, + threatIndex: undefined, timelineId: undefined, timelineTitle: undefined, timestampOverride: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 6323938d6903b..6ce0be54a9e7b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -18,6 +18,7 @@ import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; import { PartialFilter } from '../types'; import { BadRequestError } from '../errors/bad_request_error'; +import { QueryFilter } from './types'; interface GetFilterArgs { type: Type; @@ -48,7 +49,7 @@ export const getFilter = async ({ type, query, lists, -}: GetFilterArgs): Promise => { +}: GetFilterArgs): Promise => { const queryFilter = () => { if (query != null && language != null && index != null) { return getQueryFilter(query, language, filters || [], index, lists); @@ -90,6 +91,7 @@ export const getFilter = async ({ switch (type) { case 'eql': + case 'threat_match': case 'threshold': { return savedId != null ? savedQueryFilter() : queryFilter(); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 0cf0c3880fc98..68c6a51b4e6f6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -22,6 +22,7 @@ import uuid from 'uuid'; import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; import { listMock } from '../../../../../lists/server/mocks'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { BulkResponse } from './types'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -759,7 +760,7 @@ describe('searchAfterAndBulkCreate', () => { ], }) .mockImplementation(() => { - throw Error('Fake Error'); + throw Error('Fake Error'); // throws the exception we are testing }); listClient.getListItemByValues = jest.fn(({ value }) => Promise.resolve( @@ -811,4 +812,114 @@ describe('searchAfterAndBulkCreate', () => { expect(createdSignalsCount).toEqual(0); // should not create signals if search threw error expect(lastLookBackDate).toEqual(null); }); + + test('it returns error array when singleSearchAfter returns errors', async () => { + const sampleParams = sampleRuleAlertParams(30); + const bulkItem: BulkResponse = { + took: 100, + errors: true, + items: [ + { + create: { + _version: 1, + _index: 'index-123', + _id: 'id-123', + status: 201, + error: { + type: 'network', + reason: 'error on creation', + shard: 'shard-123', + index: 'index-123', + }, + }, + }, + ], + }; + mockService.callCluster + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) + .mockResolvedValueOnce(bulkItem) // adds the response with errors we are testing + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + { + create: { + status: 201, + }, + }, + ], + }) + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + { + create: { + status: 201, + }, + }, + ], + }) + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(9, 12))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + { + create: { + status: 201, + }, + }, + ], + }) + .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + + const { + success, + createdSignalsCount, + lastLookBackDate, + errors, + } = await searchAfterAndBulkCreate({ + ruleParams: sampleParams, + gap: null, + previousStartedAt: new Date(), + listClient, + exceptionsList: [], + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + inputIndexPattern, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + actions: [], + createdAt: '2020-01-28T15:58:34.810Z', + updatedAt: '2020-01-28T15:59:14.004Z', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + refresh: false, + tags: ['some fake tag 1', 'some fake tag 2'], + throttle: 'no_actions', + buildRuleMessage, + }); + expect(success).toEqual(false); + expect(errors).toEqual(['error on creation']); + expect(mockService.callCluster).toHaveBeenCalledTimes(9); + expect(createdSignalsCount).toEqual(4); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index be1c44de593a4..756aedd5273d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -51,6 +51,7 @@ export interface SearchAfterAndBulkCreateReturnType { bulkCreateTimes: string[]; lastLookBackDate: Date | null | undefined; createdSignalsCount: number; + errors: string[]; } // search_after through documents and re-index using bulk endpoint. @@ -81,11 +82,12 @@ export const searchAfterAndBulkCreate = async ({ buildRuleMessage, }: SearchAfterAndBulkCreateParams): Promise => { const toReturn: SearchAfterAndBulkCreateReturnType = { - success: false, + success: true, searchAfterTimes: [], bulkCreateTimes: [], lastLookBackDate: null, createdSignalsCount: 0, + errors: [], }; // sortId tells us where to start our next consecutive search_after query @@ -111,6 +113,7 @@ export const searchAfterAndBulkCreate = async ({ if (tuple == null || tuple.to == null || tuple.from == null) { logger.error(buildRuleMessage(`[-] malformed date tuple`)); toReturn.success = false; + toReturn.errors = [...new Set([...toReturn.errors, 'malformed date tuple'])]; return toReturn; } signalsCreatedCount = 0; @@ -163,7 +166,6 @@ export const searchAfterAndBulkCreate = async ({ } was 0, exiting and moving on to next tuple` ) ); - toReturn.success = true; break; } toReturn.lastLookBackDate = @@ -199,6 +201,8 @@ export const searchAfterAndBulkCreate = async ({ const { bulkCreateDuration: bulkDuration, createdItemsCount: createdCount, + success: bulkSuccess, + errors: bulkErrors, } = await singleBulkCreate({ filteredEvents, ruleParams, @@ -229,6 +233,8 @@ export const searchAfterAndBulkCreate = async ({ logger.debug( buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) ); + toReturn.success = toReturn.success && bulkSuccess; + toReturn.errors = [...new Set([...toReturn.errors, ...bulkErrors])]; } // we are guaranteed to have searchResult hits at this point @@ -239,17 +245,16 @@ export const searchAfterAndBulkCreate = async ({ sortId = lastSortId[0]; } else { logger.debug(buildRuleMessage('sortIds was empty on searchResult')); - toReturn.success = true; break; } - } catch (exc) { + } catch (exc: unknown) { logger.error(buildRuleMessage(`[-] search_after and bulk threw an error ${exc}`)); toReturn.success = false; + toReturn.errors = [...new Set([...toReturn.errors, `${exc}`])]; return toReturn; } } } logger.debug(buildRuleMessage(`[+] completed bulk index of ${toReturn.createdSignalsCount}`)); - toReturn.success = true; return toReturn; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts index 0c56ed300cb48..c8f8341392553 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts @@ -42,6 +42,7 @@ export const getSignalParamsSchemaDecodedMock = (): SignalParamsSchema => ({ savedId: null, severity: 'high', severityMapping: null, + threatFilters: null, threat: null, timelineId: null, timelineTitle: null, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index d08ca90f3e353..dbb48d59d3a3f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -48,6 +48,10 @@ const signalSchema = schema.object({ lists: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), // For backwards compatibility with customers that had a data bug in 7.7. Once we use a migration script please remove this. exceptions_list: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), // For backwards compatibility with customers that had a data bug in 7.8. Once we use a migration script please remove this. exceptionsList: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + threatFilters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + threatIndex: schema.maybe(schema.string()), + threatQuery: schema.maybe(schema.string()), + threatMapping: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), }); /** diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index b8311182f3ca8..3ff5d5d2a6e13 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -19,7 +19,10 @@ import { } from './utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { RuleExecutorOptions } from './types'; -import { searchAfterAndBulkCreate } from './search_after_bulk_create'; +import { + searchAfterAndBulkCreate, + SearchAfterAndBulkCreateReturnType, +} from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; import { RuleAlertType } from '../rules/types'; import { findMlSignals } from './find_ml_signals'; @@ -37,7 +40,6 @@ jest.mock('./utils'); jest.mock('../notifications/schedule_notification_actions'); jest.mock('./find_ml_signals'); jest.mock('./bulk_create_ml_signals'); -jest.mock('./../../../../common/detection_engine/utils'); jest.mock('../../../../common/detection_engine/parse_schedule_dates'); const getPayload = (ruleAlert: RuleAlertType, services: AlertServicesMock) => ({ @@ -477,21 +479,36 @@ describe('rules_notification_alert_type', () => { ); }); }); + + describe('threat match', () => { + it('should throw an error if threatQuery or threatIndex or threatMapping was not null', async () => { + const result = getResult(); + result.params.type = 'threat_match'; + payload = getPayload(result, alertServices) as jest.Mocked; + await alert.executor(payload); + expect(logger.error).toHaveBeenCalled(); + expect(logger.error.mock.calls[0][0]).toContain( + 'An error occurred during rule execution: message: "Threat Match rule is missing threatQuery and/or threatIndex and/or threatMapping: threatQuery: "undefined" threatIndex: "undefined" threatMapping: "undefined"" name: "Detect Root/Admin Users" id: "04128c15-0d1b-4716-a4c5-46997ac7f3bd" rule id: "rule-1" signals index: ".siem-signals"' + ); + }); + }); }); describe('should catch error', () => { it('when bulk indexing failed', async () => { - (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ + const result: SearchAfterAndBulkCreateReturnType = { success: false, searchAfterTimes: [], bulkCreateTimes: [], lastLookBackDate: null, createdSignalsCount: 0, - }); + errors: ['Error that bubbled up.'], + }; + (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue(result); await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); expect(logger.error.mock.calls[0][0]).toContain( - 'Bulk Indexing of signals failed. Check logs for further details.' + 'Bulk Indexing of signals failed: Error that bubbled up. name: "Detect Root/Admin Users" id: "04128c15-0d1b-4716-a4c5-46997ac7f3bd" rule id: "rule-1" signals index: ".siem-signals"' ); expect(ruleStatusService.error).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index d48b5b434c9c0..196c17b42221b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -14,7 +14,11 @@ import { SERVER_APP_ID, } from '../../../../common/constants'; import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers'; -import { isThresholdRule, isEqlRule } from '../../../../common/detection_engine/utils'; +import { + isThresholdRule, + isEqlRule, + isThreatMatchRule, +} from '../../../../common/detection_engine/utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; @@ -45,6 +49,7 @@ import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client'; import { getNotificationResultsLink } from '../notifications/utils'; +import { createThreatSignals } from './threat_mapping/create_threat_signals'; export const signalRulesAlertType = ({ logger, @@ -90,6 +95,10 @@ export const signalRulesAlertType = ({ query, to, threshold, + threatFilters, + threatQuery, + threatIndex, + threatMapping, type, exceptionsList, } = params; @@ -101,6 +110,7 @@ export const signalRulesAlertType = ({ searchAfterTimes: [], lastLookBackDate: null, createdSignalsCount: 0, + errors: [], }; const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient); const ruleStatusService = await ruleStatusServiceFactory({ @@ -221,7 +231,12 @@ export const signalRulesAlertType = ({ logger.info(buildRuleMessage(`Found ${anomalyCount} signals from ML anomalies.`)); } - const { success, bulkCreateDuration, createdItemsCount } = await bulkCreateMlSignals({ + const { + success, + errors, + bulkCreateDuration, + createdItemsCount, + } = await bulkCreateMlSignals({ actions, throttle, someResult: anomalyResults, @@ -241,6 +256,7 @@ export const signalRulesAlertType = ({ tags, }); result.success = success; + result.errors = errors; result.createdSignalsCount = createdItemsCount; if (bulkCreateDuration) { result.bulkCreateTimes.push(bulkCreateDuration); @@ -274,6 +290,7 @@ export const signalRulesAlertType = ({ success, bulkCreateDuration, createdItemsCount, + errors, } = await bulkCreateThresholdSignals({ actions, throttle, @@ -297,10 +314,62 @@ export const signalRulesAlertType = ({ tags, }); result.success = success; + result.errors = errors; result.createdSignalsCount = createdItemsCount; if (bulkCreateDuration) { result.bulkCreateTimes.push(bulkCreateDuration); } + } else if (isThreatMatchRule(type)) { + if ( + threatQuery == null || + threatIndex == null || + threatMapping == null || + query == null + ) { + throw new Error( + [ + 'Threat Match rule is missing threatQuery and/or threatIndex and/or threatMapping:', + `threatQuery: "${threatQuery}"`, + `threatIndex: "${threatIndex}"`, + `threatMapping: "${threatMapping}"`, + ].join(' ') + ); + } + const inputIndex = await getInputIndex(services, version, index); + result = await createThreatSignals({ + threatMapping, + query, + inputIndex, + type, + filters: filters ?? [], + language, + name, + savedId, + services, + exceptionItems: exceptionItems ?? [], + gap, + previousStartedAt, + listClient, + logger, + alertId, + outputIndex, + params, + searchAfterSize, + actions, + createdBy, + createdAt, + updatedBy, + interval, + updatedAt, + enabled, + refresh, + tags, + throttle, + threatFilters: threatFilters ?? [], + threatQuery, + buildRuleMessage, + threatIndex, + }); } else { const inputIndex = await getInputIndex(services, version, index); const esFilter = await getFilter({ @@ -391,7 +460,8 @@ export const signalRulesAlertType = ({ } } else { const errorMessage = buildRuleMessage( - 'Bulk Indexing of signals failed. Check logs for further details.' + 'Bulk Indexing of signals failed:', + result.errors.join() ); logger.error(errorMessage); await ruleStatusService.error(errorMessage, { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts index 41c825ea4d978..374b967d1e77f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -252,11 +252,11 @@ describe('singleBulkCreate', () => { expect(createdItemsCount).toEqual(1); }); - test('create successful bulk create when bulk create has multiple error statuses', async () => { + test('create failed bulk create when bulk create has multiple error statuses', async () => { const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockResolvedValue(sampleBulkCreateErrorResult); - const { success, createdItemsCount } = await singleBulkCreate({ + const { success, createdItemsCount, errors } = await singleBulkCreate({ filteredEvents: sampleSearchResult(), ruleParams: sampleParams, services: mockService, @@ -275,9 +275,9 @@ describe('singleBulkCreate', () => { tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(success).toEqual(true); + expect(errors).toEqual(['[4]: internal server error']); + expect(success).toEqual(false); expect(createdItemsCount).toEqual(1); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts index be71c67615a4c..e8f254e6a8966 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts @@ -63,6 +63,7 @@ export interface SingleBulkCreateResponse { success: boolean; bulkCreateDuration?: string; createdItemsCount: number; + errors: string[]; } // Bulk Index documents. @@ -89,7 +90,7 @@ export const singleBulkCreate = async ({ logger.debug(`about to bulk create ${filteredEvents.hits.hits.length} events`); if (filteredEvents.hits.hits.length === 0) { logger.debug(`all events were duplicates`); - return { success: true, createdItemsCount: 0 }; + return { success: true, createdItemsCount: 0, errors: [] }; } // index documents after creating an ID based on the // source documents' originating index, and the original @@ -138,18 +139,31 @@ export const singleBulkCreate = async ({ logger.debug(`individual bulk process time took: ${makeFloatString(end - start)} milliseconds`); logger.debug(`took property says bulk took: ${response.took} milliseconds`); - if (response.errors) { - const duplicateSignalsCount = countBy(response.items, 'create.status')['409']; + const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0; + const duplicateSignalsCount = countBy(response.items, 'create.status')['409']; + const errorCountByMessage = errorAggregator(response, [409]); + + logger.debug(`bulk created ${createdItemsCount} signals`); + if (duplicateSignalsCount > 0) { logger.debug(`ignored ${duplicateSignalsCount} duplicate signals`); - const errorCountByMessage = errorAggregator(response, [409]); - if (!isEmpty(errorCountByMessage)) { - logger.error( - `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` - ); - } } - const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0; - logger.debug(`bulk created ${createdItemsCount} signals`); - return { success: true, bulkCreateDuration: makeFloatString(end - start), createdItemsCount }; + if (!isEmpty(errorCountByMessage)) { + logger.error( + `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}` + ); + return { + errors: Object.keys(errorCountByMessage), + success: false, + bulkCreateDuration: makeFloatString(end - start), + createdItemsCount, + }; + } else { + return { + errors: [], + success: true, + bulkCreateDuration: makeFloatString(end - start), + createdItemsCount, + }; + } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts new file mode 100644 index 0000000000000..b1fab34d66ab8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; +import { Filter } from 'src/plugins/data/common'; + +import { SearchResponse } from 'elasticsearch'; +import { ThreatListItem } from './types'; + +export const getThreatMappingMock = (): ThreatMapping => { + return [ + { + entries: [ + { + field: 'host.name', + type: 'mapping', + value: 'host.name', + }, + { + field: 'host.ip', + type: 'mapping', + value: 'host.ip', + }, + ], + }, + { + entries: [ + { + field: 'destination.ip', + type: 'mapping', + value: 'destination.ip', + }, + { + field: 'destination.port', + type: 'mapping', + value: 'destination.port', + }, + ], + }, + { + entries: [ + { + field: 'source.port', + type: 'mapping', + value: 'source.port', + }, + ], + }, + { + entries: [ + { + field: 'source.ip', + type: 'mapping', + value: 'source.ip', + }, + ], + }, + ]; +}; + +export const getThreatListSearchResponseMock = (): SearchResponse => ({ + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 0, + hits: [ + { + _index: 'index', + _type: 'type', + _id: '123', + _score: 0, + _source: getThreatListItemMock(), + }, + ], + }, +}); + +export const getThreatListItemMock = (): ThreatListItem => ({ + '@timestamp': '2020-09-09T21:59:13Z', + host: { + name: 'host-1', + ip: '192.168.0.0.1', + }, + source: { + ip: '127.0.0.1', + port: 1, + }, + destination: { + ip: '127.0.0.1', + port: 1, + }, +}); + +export const getFilterThreatMapping = (): ThreatMapping => [ + { + entries: [ + { + field: 'host.name', + type: 'mapping', + value: 'host.name', + }, + { + field: 'host.ip', + type: 'mapping', + value: 'host.ip', + }, + ], + }, + { + entries: [ + { + field: 'destination.ip', + type: 'mapping', + value: 'destination.ip', + }, + { + field: 'destination.port', + type: 'mapping', + value: 'destination.port', + }, + ], + }, + { + entries: [ + { + field: 'source.port', + type: 'mapping', + value: 'source.port', + }, + ], + }, + { + entries: [ + { + field: 'source.ip', + type: 'mapping', + value: 'source.ip', + }, + ], + }, +]; + +export const getThreatMappingFilterMock = (): Filter => ({ + meta: { + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: getThreatMappingFiltersShouldMock(), + minimum_should_match: 1, + }, + }, +}); + +export const getThreatMappingFiltersShouldMock = (count = 1) => { + return new Array(count).fill(null).map((_, index) => getThreatMappingFilterShouldMock(index + 1)); +}; + +export const getThreatMappingFilterShouldMock = (port = 1) => ({ + bool: { + should: [ + { + bool: { + filter: [ + { + bool: { + should: [{ match: { 'host.name': 'host-1' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'host.ip': '192.168.0.0.1' } }], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [{ match: { 'destination.ip': '127.0.0.1' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'destination.port': port } }], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [{ match: { 'source.port': port } }], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [{ match: { 'source.ip': '127.0.0.1' } }], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + }, +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts new file mode 100644 index 0000000000000..cf4a570248c99 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts @@ -0,0 +1,457 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ThreatMapping, + ThreatMappingEntries, +} from '../../../../../common/detection_engine/schemas/types/threat_mapping'; + +import { + filterThreatMapping, + buildThreatMappingFilter, + splitShouldClauses, + createInnerAndClauses, + createAndOrClauses, + buildEntriesMappingFilter, +} from './build_threat_mapping_filter'; +import { + getThreatMappingMock, + getThreatListSearchResponseMock, + getThreatListItemMock, + getThreatMappingFilterMock, + getFilterThreatMapping, + getThreatMappingFiltersShouldMock, + getThreatMappingFilterShouldMock, +} from './build_threat_mapping_filter.mock'; +import { BooleanFilter } from './types'; + +describe('build_threat_mapping_filter', () => { + describe('buildThreatMappingFilter', () => { + test('it should throw if given a chunk over 1024 in size', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock(); + expect(() => + buildThreatMappingFilter({ threatMapping, threatList, chunkSize: 1025 }) + ).toThrow('chunk sizes cannot exceed 1024 in size'); + }); + + test('it should NOT throw if given a chunk under 1024 in size', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock(); + expect(() => + buildThreatMappingFilter({ threatMapping, threatList, chunkSize: 1023 }) + ).not.toThrow(); + }); + + test('it should create the correct entries when using the default mocks', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock(); + const filter = buildThreatMappingFilter({ threatMapping, threatList }); + expect(filter).toEqual(getThreatMappingFilterMock()); + }); + + test('it should not mutate the original threatMapping', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock(); + buildThreatMappingFilter({ threatMapping, threatList }); + expect(threatMapping).toEqual(getThreatMappingMock()); + }); + + test('it should not mutate the original threatListItem', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock(); + buildThreatMappingFilter({ threatMapping, threatList }); + expect(threatList).toEqual(getThreatListSearchResponseMock()); + }); + }); + + describe('filterThreatMapping', () => { + test('it should not remove any entries when using the default mocks', () => { + const threatMapping = getThreatMappingMock(); + const threatListItem = getThreatListItemMock(); + + const item = filterThreatMapping({ threatMapping, threatListItem }); + const expected = getFilterThreatMapping(); + expect(item).toEqual(expected); + }); + + test('it should only give one filtered element if only 1 element is defined', () => { + const [firstElement] = getThreatMappingMock(); // get only the first element + const threatListItem = getThreatListItemMock(); + + const item = filterThreatMapping({ threatMapping: [firstElement], threatListItem }); + const [firstElementFilter] = getFilterThreatMapping(); // get only the first element to compare + expect(item).toEqual([firstElementFilter]); + }); + + test('it should not mutate the original threatMapping', () => { + const threatMapping = getThreatMappingMock(); + const threatListItem = getThreatListItemMock(); + + filterThreatMapping({ + threatMapping, + threatListItem, + }); + expect(threatMapping).toEqual(getThreatMappingMock()); + }); + + test('it should not mutate the original threatListItem', () => { + const threatMapping = getThreatMappingMock(); + const threatListItem = getThreatListItemMock(); + + filterThreatMapping({ + threatMapping, + threatListItem, + }); + expect(threatListItem).toEqual(getThreatListItemMock()); + }); + }); + + describe('createInnerAndClauses', () => { + test('it should return two clauses given a single entry', () => { + const [{ entries: threatMappingEntries }] = getThreatMappingMock(); // get the first element + const threatListItem = getThreatListItemMock(); + const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem }); + const { + bool: { + should: [ + { + bool: { filter }, + }, + ], + }, + } = getThreatMappingFilterShouldMock(); // get the first element + expect(innerClause).toEqual(filter); + }); + + test('it should return an empty array given an empty array', () => { + const threatListItem = getThreatListItemMock(); + const innerClause = createInnerAndClauses({ threatMappingEntries: [], threatListItem }); + expect(innerClause).toEqual([]); + }); + + test('it should filter out a single unknown value', () => { + const [{ entries }] = getThreatMappingMock(); // get the first element + const threatMappingEntries: ThreatMappingEntries = [ + ...entries, + { + field: 'host.name', // add second invalid entry which should be filtered away + value: 'invalid', + type: 'mapping', + }, + ]; + const threatListItem = getThreatListItemMock(); + const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem }); + const { + bool: { + should: [ + { + bool: { filter }, + }, + ], + }, + } = getThreatMappingFilterShouldMock(); // get the first element + expect(innerClause).toEqual(filter); + }); + + test('it should filter out 2 unknown values', () => { + const [{ entries }] = getThreatMappingMock(); // get the first element + const threatMappingEntries: ThreatMappingEntries = [ + ...entries, + { + field: 'host.name', // add second invalid entry which should be filtered away + value: 'invalid', + type: 'mapping', + }, + { + field: 'host.ip', // add second invalid entry which should be filtered away + value: 'invalid', + type: 'mapping', + }, + ]; + const threatListItem = getThreatListItemMock(); + const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem }); + const { + bool: { + should: [ + { + bool: { filter }, + }, + ], + }, + } = getThreatMappingFilterShouldMock(); // get the first element + expect(innerClause).toEqual(filter); + }); + + test('it should filter out all unknown values as an empty array', () => { + const threatMappingEntries: ThreatMappingEntries = [ + { + field: 'host.name', // add second invalid entry which should be filtered away + value: 'invalid', + type: 'mapping', + }, + { + field: 'host.ip', // add second invalid entry which should be filtered away + value: 'invalid', + type: 'mapping', + }, + ]; + const threatListItem = getThreatListItemMock(); + const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem }); + expect(innerClause).toEqual([]); + }); + }); + + describe('createAndOrClauses', () => { + test('it should return all clauses given the entries', () => { + const threatMapping = getThreatMappingMock(); + const threatListItem = getThreatListItemMock(); + const innerClause = createAndOrClauses({ threatMapping, threatListItem }); + expect(innerClause).toEqual(getThreatMappingFilterShouldMock()); + }); + + test('it should filter out data from entries that do not have mappings', () => { + const threatMapping = getThreatMappingMock(); + const threatListItem = { ...getThreatListItemMock(), foo: 'bar' }; + const innerClause = createAndOrClauses({ threatMapping, threatListItem }); + expect(innerClause).toEqual(getThreatMappingFilterShouldMock()); + }); + + test('it should return an empty boolean given an empty array', () => { + const threatListItem = getThreatListItemMock(); + const innerClause = createAndOrClauses({ threatMapping: [], threatListItem }); + expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } }); + }); + + test('it should return an empty boolean clause given an empty object for a threat list item', () => { + const threatMapping = getThreatMappingMock(); + const innerClause = createAndOrClauses({ threatMapping, threatListItem: {} }); + expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } }); + }); + }); + + describe('buildEntriesMappingFilter', () => { + test('it should return all clauses given the entries', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock(); + const mapping = buildEntriesMappingFilter({ + threatMapping, + threatList, + chunkSize: 1024, + }); + const expected: BooleanFilter = { + bool: { should: [getThreatMappingFilterShouldMock()], minimum_should_match: 1 }, + }; + expect(mapping).toEqual(expected); + }); + + test('it should return empty "should" given an empty threat list', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock(); + threatList.hits.hits = []; + const mapping = buildEntriesMappingFilter({ + threatMapping, + threatList, + chunkSize: 1024, + }); + const expected: BooleanFilter = { + bool: { should: [], minimum_should_match: 1 }, + }; + expect(mapping).toEqual(expected); + }); + + test('it should return empty "should" given an empty threat mapping', () => { + const threatList = getThreatListSearchResponseMock(); + const mapping = buildEntriesMappingFilter({ + threatMapping: [], + threatList, + chunkSize: 1024, + }); + const expected: BooleanFilter = { + bool: { should: [], minimum_should_match: 1 }, + }; + expect(mapping).toEqual(expected); + }); + + test('it should ignore entries that are invalid', () => { + const entries: ThreatMappingEntries = [ + { + field: 'host.name', + type: 'mapping', + value: 'invalid', + }, + { + field: 'host.ip', + type: 'mapping', + value: 'invalid', + }, + ]; + + const threatMapping: ThreatMapping = [ + ...getThreatMappingMock(), + ...[ + { + entries, + }, + ], + ]; + const threatList = getThreatListSearchResponseMock(); + const mapping = buildEntriesMappingFilter({ + threatMapping, + threatList, + chunkSize: 1024, + }); + const expected: BooleanFilter = { + bool: { should: [getThreatMappingFilterShouldMock()], minimum_should_match: 1 }, + }; + expect(mapping).toEqual(expected); + }); + }); + + describe('splitShouldClauses', () => { + test('it should NOT split a single should clause as there is nothing to split on with chunkSize 1', () => { + const should = getThreatMappingFiltersShouldMock(); + const clauses = splitShouldClauses({ should, chunkSize: 1 }); + expect(clauses).toEqual(getThreatMappingFiltersShouldMock()); + }); + + test('it should NOT mutate the original should clause passed in', () => { + const should = getThreatMappingFiltersShouldMock(); + expect(should).toEqual(getThreatMappingFiltersShouldMock()); + }); + + test('it should NOT split a single should clause as there is nothing to split on with chunkSize 2', () => { + const should = getThreatMappingFiltersShouldMock(); + const clauses = splitShouldClauses({ should, chunkSize: 2 }); + expect(clauses).toEqual(getThreatMappingFiltersShouldMock()); + }); + + test('it should return an empty array given an empty array', () => { + const clauses = splitShouldClauses({ should: [], chunkSize: 2 }); + expect(clauses).toEqual([]); + }); + + test('it should split an array of size 2 into a length 2 array with chunks on "chunkSize: 1"', () => { + const should = getThreatMappingFiltersShouldMock(2); + const clauses = splitShouldClauses({ should, chunkSize: 1 }); + expect(clauses.length).toEqual(2); + }); + + test('it should not mutate the original when splitting on chunks', () => { + const should = getThreatMappingFiltersShouldMock(2); + splitShouldClauses({ should, chunkSize: 1 }); + expect(should).toEqual(getThreatMappingFiltersShouldMock(2)); + }); + + test('it should split an array of size 2 into 2 different chunks on "chunkSize: 1"', () => { + const should = getThreatMappingFiltersShouldMock(2); + const clauses = splitShouldClauses({ should, chunkSize: 1 }); + const expected: BooleanFilter[] = [ + { + bool: { + should: [getThreatMappingFilterShouldMock(1)], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [getThreatMappingFilterShouldMock(2)], + minimum_should_match: 1, + }, + }, + ]; + expect(clauses).toEqual(expected); + }); + + test('it should split an array of size 4 into 4 groups of 4 chunks on "chunkSize: 1"', () => { + const should = getThreatMappingFiltersShouldMock(4); + const clauses = splitShouldClauses({ should, chunkSize: 1 }); + const expected: BooleanFilter[] = [ + { + bool: { + should: [getThreatMappingFilterShouldMock(1)], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [getThreatMappingFilterShouldMock(2)], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [getThreatMappingFilterShouldMock(3)], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [getThreatMappingFilterShouldMock(4)], + minimum_should_match: 1, + }, + }, + ]; + expect(clauses).toEqual(expected); + }); + + test('it should split an array of size 4 into 2 groups of 2 chunks on "chunkSize: 2"', () => { + const should = getThreatMappingFiltersShouldMock(4); + const clauses = splitShouldClauses({ should, chunkSize: 2 }); + const expected: BooleanFilter[] = [ + { + bool: { + should: [getThreatMappingFilterShouldMock(1), getThreatMappingFilterShouldMock(2)], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [getThreatMappingFilterShouldMock(3), getThreatMappingFilterShouldMock(4)], + minimum_should_match: 1, + }, + }, + ]; + expect(clauses).toEqual(expected); + }); + + test('it should NOT split an array of size 4 into any groups on "chunkSize: 5"', () => { + const should = getThreatMappingFiltersShouldMock(4); + const clauses = splitShouldClauses({ should, chunkSize: 5 }); + const expected: BooleanFilter[] = [ + getThreatMappingFilterShouldMock(1), + getThreatMappingFilterShouldMock(2), + getThreatMappingFilterShouldMock(3), + getThreatMappingFilterShouldMock(4), + ]; + expect(clauses).toEqual(expected); + }); + + test('it should split an array of size 4 into 2 groups on "chunkSize: 3"', () => { + const should = getThreatMappingFiltersShouldMock(4); + const clauses = splitShouldClauses({ should, chunkSize: 3 }); + const expected: BooleanFilter[] = [ + { + bool: { + should: [ + getThreatMappingFilterShouldMock(1), + getThreatMappingFilterShouldMock(2), + getThreatMappingFilterShouldMock(3), + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [getThreatMappingFilterShouldMock(4)], + minimum_should_match: 1, + }, + }, + ]; + expect(clauses).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts new file mode 100644 index 0000000000000..3299b6ae34e4d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import get from 'lodash/fp/get'; +import { Filter } from 'src/plugins/data/common'; +import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; +import { + BooleanFilter, + BuildEntriesMappingFilterOptions, + BuildThreatMappingFilterOptions, + CreateAndOrClausesOptions, + CreateInnerAndClausesOptions, + FilterThreatMappingOptions, + SplitShouldClausesOptions, +} from './types'; + +export const MAX_CHUNK_SIZE = 1024; + +export const buildThreatMappingFilter = ({ + threatMapping, + threatList, + chunkSize, +}: BuildThreatMappingFilterOptions): Filter => { + const computedChunkSize = chunkSize ?? MAX_CHUNK_SIZE; + if (computedChunkSize > 1024) { + throw new TypeError('chunk sizes cannot exceed 1024 in size'); + } + const query = buildEntriesMappingFilter({ + threatMapping, + threatList, + chunkSize: computedChunkSize, + }); + const filterChunk: Filter = { + meta: { + alias: null, + negate: false, + disabled: false, + }, + query, + }; + return filterChunk; +}; + +/** + * Filters out any entries which do not include the threat list item. + */ +export const filterThreatMapping = ({ + threatMapping, + threatListItem, +}: FilterThreatMappingOptions): ThreatMapping => + threatMapping + .map((threatMap) => { + const entries = threatMap.entries.filter((entry) => get(entry.value, threatListItem) != null); + return { ...threatMap, entries }; + }) + .filter((threatMap) => threatMap.entries.length !== 0); + +export const createInnerAndClauses = ({ + threatMappingEntries, + threatListItem, +}: CreateInnerAndClausesOptions): BooleanFilter[] => { + return threatMappingEntries.reduce((accum, threatMappingEntry) => { + const value = get(threatMappingEntry.value, threatListItem); + if (value != null) { + // These values could be potentially 10k+ large so mutating the array intentionally + accum.push({ + bool: { + should: [ + { + match: { + [threatMappingEntry.field]: value, + }, + }, + ], + minimum_should_match: 1, + }, + }); + } + return accum; + }, []); +}; + +export const createAndOrClauses = ({ + threatMapping, + threatListItem, +}: CreateAndOrClausesOptions): BooleanFilter => { + const should = threatMapping.reduce((accum, threatMap) => { + const innerAndClauses = createInnerAndClauses({ + threatMappingEntries: threatMap.entries, + threatListItem, + }); + if (innerAndClauses.length !== 0) { + // These values could be potentially 10k+ large so mutating the array intentionally + accum.push({ + bool: { filter: innerAndClauses }, + }); + } + return accum; + }, []); + return { bool: { should, minimum_should_match: 1 } }; +}; + +export const buildEntriesMappingFilter = ({ + threatMapping, + threatList, + chunkSize, +}: BuildEntriesMappingFilterOptions): BooleanFilter => { + const combinedShould = threatList.hits.hits.reduce( + (accum, threatListSearchItem) => { + const filteredEntries = filterThreatMapping({ + threatMapping, + threatListItem: threatListSearchItem._source, + }); + const queryWithAndOrClause = createAndOrClauses({ + threatMapping: filteredEntries, + threatListItem: threatListSearchItem._source, + }); + if (queryWithAndOrClause.bool.should.length !== 0) { + // These values can be 10k+ large, so using a push here for performance + accum.push(queryWithAndOrClause); + } + return accum; + }, + [] + ); + const should = splitShouldClauses({ should: combinedShould, chunkSize }); + return { bool: { should, minimum_should_match: 1 } }; +}; + +export const splitShouldClauses = ({ + should, + chunkSize, +}: SplitShouldClausesOptions): BooleanFilter[] => { + if (should.length <= chunkSize) { + return should; + } else { + return should.reduce((accum, item, index) => { + const chunkIndex = Math.floor(index / chunkSize); + const currentChunk = accum[chunkIndex]; + if (!currentChunk) { + // create a new element in the array at the correct spot + accum[chunkIndex] = { bool: { should: [], minimum_should_match: 1 } }; + } + // Add to the existing array element. Using mutatious push here since these arrays can get very large such as 10k+ and this is going to be a hot code spot. + accum[chunkIndex].bool.should.push(item); + return accum; + }, []); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts new file mode 100644 index 0000000000000..7542128d83769 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { getThreatList } from './get_threat_list'; +import { buildThreatMappingFilter } from './build_threat_mapping_filter'; + +import { getFilter } from '../get_filter'; +import { + searchAfterAndBulkCreate, + SearchAfterAndBulkCreateReturnType, +} from '../search_after_bulk_create'; +import { CreateThreatSignalOptions, ThreatListItem } from './types'; +import { combineResults } from './utils'; + +export const createThreatSignal = async ({ + threatMapping, + query, + inputIndex, + type, + filters, + language, + savedId, + services, + exceptionItems, + gap, + previousStartedAt, + listClient, + logger, + alertId, + outputIndex, + params, + searchAfterSize, + actions, + createdBy, + createdAt, + updatedBy, + interval, + updatedAt, + enabled, + refresh, + tags, + throttle, + threatFilters, + threatQuery, + buildRuleMessage, + threatIndex, + name, + currentThreatList, + currentResult, +}: CreateThreatSignalOptions): Promise<{ + threatList: SearchResponse; + results: SearchAfterAndBulkCreateReturnType; +}> => { + const threatFilter = buildThreatMappingFilter({ + threatMapping, + threatList: currentThreatList, + }); + + const esFilter = await getFilter({ + type, + filters: [...filters, threatFilter], + language, + query, + savedId, + services, + index: inputIndex, + lists: exceptionItems, + }); + + const newResult = await searchAfterAndBulkCreate({ + gap, + previousStartedAt, + listClient, + exceptionsList: exceptionItems, + ruleParams: params, + services, + logger, + id: alertId, + inputIndexPattern: inputIndex, + signalsIndex: outputIndex, + filter: esFilter, + actions, + name, + createdBy, + createdAt, + updatedBy, + updatedAt, + interval, + enabled, + pageSize: searchAfterSize, + refresh, + tags, + throttle, + buildRuleMessage, + }); + + const results = combineResults(currentResult, newResult); + const searchAfter = currentThreatList.hits.hits[currentThreatList.hits.hits.length - 1].sort; + + const threatList = await getThreatList({ + callCluster: services.callCluster, + exceptionItems, + query: threatQuery, + threatFilters, + index: [threatIndex], + searchAfter, + sortField: undefined, + sortOrder: undefined, + }); + + return { threatList, results }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts new file mode 100644 index 0000000000000..9027475d71c4a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getThreatList } from './get_threat_list'; + +import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; +import { CreateThreatSignalsOptions } from './types'; +import { createThreatSignal } from './create_threat_signal'; + +export const createThreatSignals = async ({ + threatMapping, + query, + inputIndex, + type, + filters, + language, + savedId, + services, + exceptionItems, + gap, + previousStartedAt, + listClient, + logger, + alertId, + outputIndex, + params, + searchAfterSize, + actions, + createdBy, + createdAt, + updatedBy, + interval, + updatedAt, + enabled, + refresh, + tags, + throttle, + threatFilters, + threatQuery, + buildRuleMessage, + threatIndex, + name, +}: CreateThreatSignalsOptions): Promise => { + let results: SearchAfterAndBulkCreateReturnType = { + success: true, + bulkCreateTimes: [], + searchAfterTimes: [], + lastLookBackDate: null, + createdSignalsCount: 0, + errors: [], + }; + + let threatList = await getThreatList({ + callCluster: services.callCluster, + exceptionItems, + threatFilters, + query: threatQuery, + index: [threatIndex], + searchAfter: undefined, + sortField: undefined, + sortOrder: undefined, + }); + + while (threatList.hits.hits.length !== 0 && results.createdSignalsCount <= params.maxSignals) { + ({ threatList, results } = await createThreatSignal({ + threatMapping, + query, + inputIndex, + type, + filters, + language, + savedId, + services, + exceptionItems, + gap, + previousStartedAt, + listClient, + logger, + alertId, + outputIndex, + params, + searchAfterSize, + actions, + createdBy, + createdAt, + updatedBy, + updatedAt, + interval, + enabled, + tags, + refresh, + throttle, + threatFilters, + threatQuery, + buildRuleMessage, + threatIndex, + name, + currentThreatList: threatList, + currentResult: results, + })); + } + return results; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts new file mode 100644 index 0000000000000..f600463c213c2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSortWithTieBreaker } from './get_threat_list'; + +describe('get_threat_signals', () => { + describe('getSortWithTieBreaker', () => { + test('it should return sort field of just timestamp if given no sort order', () => { + const sortOrder = getSortWithTieBreaker({ sortField: undefined, sortOrder: undefined }); + expect(sortOrder).toEqual([{ '@timestamp': 'asc' }]); + }); + + test('it should return sort field of timestamp with asc even if sortOrder is changed as it is hard wired in', () => { + const sortOrder = getSortWithTieBreaker({ sortField: undefined, sortOrder: 'desc' }); + expect(sortOrder).toEqual([{ '@timestamp': 'asc' }]); + }); + + test('it should return sort field of an extra field if given one', () => { + const sortOrder = getSortWithTieBreaker({ sortField: 'some-field', sortOrder: undefined }); + expect(sortOrder).toEqual([{ 'some-field': 'asc', '@timestamp': 'asc' }]); + }); + + test('it should return sort field of desc if given one', () => { + const sortOrder = getSortWithTieBreaker({ sortField: 'some-field', sortOrder: 'desc' }); + expect(sortOrder).toEqual([{ 'some-field': 'desc', '@timestamp': 'asc' }]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts new file mode 100644 index 0000000000000..8b381ca0d96dc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; +import { + GetSortWithTieBreakerOptions, + GetThreatListOptions, + SortWithTieBreaker, + ThreatListItem, +} from './types'; + +/** + * This should not exceed 10000 (10k) + */ +export const MAX_PER_PAGE = 9000; + +export const getThreatList = async ({ + callCluster, + query, + index, + perPage, + searchAfter, + sortField, + sortOrder, + exceptionItems, + threatFilters, +}: GetThreatListOptions): Promise> => { + const calculatedPerPage = perPage ?? MAX_PER_PAGE; + if (calculatedPerPage > 10000) { + throw new TypeError('perPage cannot exceed the size of 10000'); + } + const queryFilter = getQueryFilter(query, 'kuery', threatFilters, index, exceptionItems); + const response: SearchResponse = await callCluster('search', { + body: { + query: queryFilter, + search_after: searchAfter, + sort: getSortWithTieBreaker({ sortField, sortOrder }), + }, + ignoreUnavailable: true, + index, + size: calculatedPerPage, + }); + return response; +}; + +export const getSortWithTieBreaker = ({ + sortField, + sortOrder, +}: GetSortWithTieBreakerOptions): SortWithTieBreaker[] => { + const ascOrDesc = sortOrder ?? 'asc'; + if (sortField != null) { + return [{ [sortField]: ascOrDesc, '@timestamp': 'asc' }]; + } else { + return [{ '@timestamp': 'asc' }]; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts new file mode 100644 index 0000000000000..4c3cd9943adb4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Duration } from 'moment'; +import { SearchResponse } from 'elasticsearch'; +import { ListClient } from '../../../../../../lists/server'; +import { + Type, + LanguageOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { + ThreatQuery, + ThreatMapping, + ThreatMappingEntries, +} from '../../../../../common/detection_engine/schemas/types/threat_mapping'; +import { PartialFilter, RuleTypeParams } from '../../types'; +import { AlertServices } from '../../../../../../alerts/server'; +import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; +import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; +import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/core/server'; +import { RuleAlertAction } from '../../../../../common/detection_engine/types'; +import { BuildRuleMessage } from '../rule_messages'; + +export interface CreateThreatSignalsOptions { + threatMapping: ThreatMapping; + query: string; + inputIndex: string[]; + type: Type; + filters: PartialFilter[]; + language: LanguageOrUndefined; + savedId: string | undefined; + services: AlertServices; + exceptionItems: ExceptionListItemSchema[]; + gap: Duration | null; + previousStartedAt: Date | null; + listClient: ListClient; + logger: Logger; + alertId: string; + outputIndex: string; + params: RuleTypeParams; + searchAfterSize: number; + actions: RuleAlertAction[]; + createdBy: string; + createdAt: string; + updatedBy: string; + updatedAt: string; + interval: string; + enabled: boolean; + tags: string[]; + refresh: false | 'wait_for'; + throttle: string; + threatFilters: PartialFilter[]; + threatQuery: ThreatQuery; + buildRuleMessage: BuildRuleMessage; + threatIndex: string; + name: string; +} + +export interface CreateThreatSignalOptions { + threatMapping: ThreatMapping; + query: string; + inputIndex: string[]; + type: Type; + filters: PartialFilter[]; + language: LanguageOrUndefined; + savedId: string | undefined; + services: AlertServices; + exceptionItems: ExceptionListItemSchema[]; + gap: Duration | null; + previousStartedAt: Date | null; + listClient: ListClient; + logger: Logger; + alertId: string; + outputIndex: string; + params: RuleTypeParams; + searchAfterSize: number; + actions: RuleAlertAction[]; + createdBy: string; + createdAt: string; + updatedBy: string; + updatedAt: string; + interval: string; + enabled: boolean; + tags: string[]; + refresh: false | 'wait_for'; + throttle: string; + threatFilters: PartialFilter[]; + threatQuery: ThreatQuery; + buildRuleMessage: BuildRuleMessage; + threatIndex: string; + name: string; + currentThreatList: SearchResponse; + currentResult: SearchAfterAndBulkCreateReturnType; +} + +export interface BuildThreatMappingFilterOptions { + threatMapping: ThreatMapping; + threatList: SearchResponse; + chunkSize?: number; +} + +export interface FilterThreatMappingOptions { + threatMapping: ThreatMapping; + threatListItem: ThreatListItem; +} + +export interface CreateInnerAndClausesOptions { + threatMappingEntries: ThreatMappingEntries; + threatListItem: ThreatListItem; +} + +export interface CreateAndOrClausesOptions { + threatMapping: ThreatMapping; + threatListItem: ThreatListItem; +} + +export interface BuildEntriesMappingFilterOptions { + threatMapping: ThreatMapping; + threatList: SearchResponse; + chunkSize: number; +} + +export interface SplitShouldClausesOptions { + should: BooleanFilter[]; + chunkSize: number; +} + +export interface BooleanFilter { + bool: { should: unknown[]; minimum_should_match: number }; +} + +export interface GetThreatListOptions { + callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; + query: string; + index: string[]; + perPage?: number; + searchAfter: string[] | undefined; + sortField: string | undefined; + sortOrder: 'asc' | 'desc' | undefined; + threatFilters: PartialFilter[]; + exceptionItems: ExceptionListItemSchema[]; +} + +export interface GetSortWithTieBreakerOptions { + sortField: string | undefined; + sortOrder: 'asc' | 'desc' | undefined; +} + +/** + * This is an ECS document being returned, but the user could return or use non-ecs based + * documents potentially. + */ +export interface ThreatListItem { + [key: string]: unknown; +} + +export interface SortWithTieBreaker { + '@timestamp': 'asc'; + [key: string]: string; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts new file mode 100644 index 0000000000000..48bdf430b940e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; + +import { calculateAdditiveMax, combineResults } from './utils'; + +describe('utils', () => { + describe('calculateAdditiveMax', () => { + test('it should return 0 for two empty arrays', () => { + const max = calculateAdditiveMax([], []); + expect(max).toEqual(['0']); + }); + + test('it should return 10 for two arrays with the numbers 5', () => { + const max = calculateAdditiveMax(['5'], ['5']); + expect(max).toEqual(['10']); + }); + + test('it should return 5 for two arrays with second array having just 5', () => { + const max = calculateAdditiveMax([], ['5']); + expect(max).toEqual(['5']); + }); + + test('it should return 5 for two arrays with first array having just 5', () => { + const max = calculateAdditiveMax(['5'], []); + expect(max).toEqual(['5']); + }); + + test('it should return 10 for the max of the two arrays added together when the max of each array is 5, "5 + 5 = 10"', () => { + const max = calculateAdditiveMax(['3', '5', '1'], ['3', '5', '1']); + expect(max).toEqual(['10']); + }); + }); + + describe('combineResults', () => { + test('it should combine two results with success set to "true" if both are "true"', () => { + const existingResult: SearchAfterAndBulkCreateReturnType = { + success: true, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: undefined, + createdSignalsCount: 3, + errors: [], + }; + + const newResult: SearchAfterAndBulkCreateReturnType = { + success: true, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: undefined, + createdSignalsCount: 3, + errors: [], + }; + const combinedResults = combineResults(existingResult, newResult); + expect(combinedResults.success).toEqual(true); + }); + + test('it should combine two results with success set to "false" if one of them is "false"', () => { + const existingResult: SearchAfterAndBulkCreateReturnType = { + success: false, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: undefined, + createdSignalsCount: 3, + errors: [], + }; + + const newResult: SearchAfterAndBulkCreateReturnType = { + success: true, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: undefined, + createdSignalsCount: 3, + errors: [], + }; + const combinedResults = combineResults(existingResult, newResult); + expect(combinedResults.success).toEqual(false); + }); + + test('it should use the latest date if it is set in the new result', () => { + const existingResult: SearchAfterAndBulkCreateReturnType = { + success: false, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: undefined, + createdSignalsCount: 3, + errors: [], + }; + + const newResult: SearchAfterAndBulkCreateReturnType = { + success: true, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), + createdSignalsCount: 3, + errors: [], + }; + const combinedResults = combineResults(existingResult, newResult); + expect(combinedResults.lastLookBackDate?.toISOString()).toEqual('2020-09-16T03:34:32.390Z'); + }); + + test('it should combine the searchAfterTimes and the bulkCreateTimes', () => { + const existingResult: SearchAfterAndBulkCreateReturnType = { + success: false, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: undefined, + createdSignalsCount: 3, + errors: [], + }; + + const newResult: SearchAfterAndBulkCreateReturnType = { + success: true, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), + createdSignalsCount: 3, + errors: [], + }; + const combinedResults = combineResults(existingResult, newResult); + expect(combinedResults).toEqual( + expect.objectContaining({ + searchAfterTimes: ['60'], + bulkCreateTimes: ['50'], + }) + ); + }); + + test('it should combine errors together without duplicates', () => { + const existingResult: SearchAfterAndBulkCreateReturnType = { + success: false, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: undefined, + createdSignalsCount: 3, + errors: ['error 1', 'error 2', 'error 3'], + }; + + const newResult: SearchAfterAndBulkCreateReturnType = { + success: true, + searchAfterTimes: ['10', '20', '30'], + bulkCreateTimes: ['5', '15', '25'], + lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), + createdSignalsCount: 3, + errors: ['error 4', 'error 1', 'error 3', 'error 5'], + }; + const combinedResults = combineResults(existingResult, newResult); + expect(combinedResults).toEqual( + expect.objectContaining({ + errors: ['error 1', 'error 2', 'error 3', 'error 4', 'error 5'], + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts new file mode 100644 index 0000000000000..38bbb70b6c4ec --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; + +/** + * Given two timers this will take the max of each and add them to each other and return that addition. + * Max(timer_array_1) + Max(timer_array_2) + * @param existingTimers String array of existing timers + * @param newTimers String array of new timers. + * @returns String array of the new maximum between the two timers + */ +export const calculateAdditiveMax = (existingTimers: string[], newTimers: string[]): string[] => { + const numericNewTimerMax = Math.max(0, ...newTimers.map((time) => +time)); + const numericExistingTimerMax = Math.max(0, ...existingTimers.map((time) => +time)); + return [String(numericNewTimerMax + numericExistingTimerMax)]; +}; + +/** + * Combines two results together and returns the results combined + * @param currentResult The current result to combine with a newResult + * @param newResult The new result to combine + */ +export const combineResults = ( + currentResult: SearchAfterAndBulkCreateReturnType, + newResult: SearchAfterAndBulkCreateReturnType +): SearchAfterAndBulkCreateReturnType => ({ + success: currentResult.success === false ? false : newResult.success, + bulkCreateTimes: calculateAdditiveMax(currentResult.bulkCreateTimes, newResult.bulkCreateTimes), + searchAfterTimes: calculateAdditiveMax( + currentResult.searchAfterTimes, + newResult.searchAfterTimes + ), + lastLookBackDate: newResult.lastLookBackDate, + createdSignalsCount: currentResult.createdSignalsCount + newResult.createdSignalsCount, + errors: [...new Set([...currentResult.errors, ...newResult.errors])], +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 700a8fb5022d7..23aa786558a99 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DslQuery, Filter } from 'src/plugins/data/common'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { AlertType, AlertTypeState, AlertExecutorOptions } from '../../../../../alerts/server'; @@ -58,7 +59,7 @@ export interface SignalSource { } export interface BulkItem { - create: { + create?: { _index: string; _type?: string; _id: string; @@ -166,3 +167,15 @@ export interface RuleAlertAttributes extends AlertAttributes { } export type BulkResponseErrorAggregation = Record; + +/** + * TODO: Remove this if/when the return filter has its own type exposed + */ +export interface QueryFilter { + bool: { + must: DslQuery[]; + filter: Filter[]; + should: unknown[]; + must_not: Filter[]; + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 9d22ba9dcc02b..123b9c9bdffa2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -356,6 +356,14 @@ describe('utils', () => { expect(aggregated).toEqual(expected); }); + test('it should aggregate with an empty create object', () => { + const empty = sampleBulkResponse(); + empty.items = [{}]; + const aggregated = errorAggregator(empty, []); + const expected: BulkResponseErrorAggregation = {}; + expect(aggregated).toEqual(expected); + }); + test('it should aggregate with an empty object when given a valid bulk response with no errors', () => { const validResponse = sampleBulkResponse(); const aggregated = errorAggregator(validResponse, []); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 4a6ea96e1854b..9f1e5d6980466 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -292,7 +292,7 @@ export const errorAggregator = ( ignoreStatusCodes: number[] ): BulkResponseErrorAggregation => { return response.items.reduce((accum, item) => { - if (item.create.error != null && !ignoreStatusCodes.includes(item.create.status)) { + if (item.create?.error != null && !ignoreStatusCodes.includes(item.create.status)) { if (accum[item.create.error.reason] == null) { accum[item.create.error.reason] = { count: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index cbe756064b72b..b0554adcc46b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -38,6 +38,12 @@ import { TimestampOverrideOrUndefined, Type, } from '../../../common/detection_engine/schemas/common/schemas'; +import { + ThreatIndexOrUndefined, + ThreatQueryOrUndefined, + ThreatMappingOrUndefined, +} from '../../../common/detection_engine/schemas/types/threat_mapping'; + import { LegacyCallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; import { ListArrayOrUndefined } from '../../../common/detection_engine/schemas/types'; @@ -73,6 +79,10 @@ export interface RuleTypeParams { severityMapping: SeverityMappingOrUndefined; threat: ThreatOrUndefined; threshold: ThresholdOrUndefined; + threatFilters: PartialFilter[] | undefined; + threatIndex: ThreatIndexOrUndefined; + threatQuery: ThreatQueryOrUndefined; + threatMapping: ThreatMappingOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; type: Type; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index f0e7372a208fb..0571c4878956f 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -16,6 +16,7 @@ import { Plugin as IPlugin, PluginInitializerContext, SavedObjectsClient, + DEFAULT_APP_CATEGORIES, } from '../../../../src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginSetup, DataPluginStart } from '../../../../src/plugins/data/server/plugin'; @@ -178,6 +179,7 @@ export class Plugin implements IPlugin management: { data: [PLUGIN.id], }, + catalogue: [PLUGIN.id], privileges: [ { requiredClusterPrivileges: [...APP_REQUIRED_CLUSTER_PRIVILEGES], diff --git a/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx b/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx index c9f8431fe1ab7..9500810a395f8 100644 --- a/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx +++ b/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx @@ -22,7 +22,7 @@ export const SecureSpaceMessage = (props: SecureSpaceMessageProps) => { return ( - +

{ description={this.getPanelDescription()} fullWidth > - - - - - - - - - - - - } - closePopover={this.closePopover} - {...extraPopoverProps} - ownFocus={true} - isOpen={this.state.customizingAvatar} - > -

- -
-
-
- - + + + @@ -175,6 +134,37 @@ export class CustomizeSpace extends Component { rows={2} />
+ + + + + + } + closePopover={this.closePopover} + {...extraPopoverProps} + ownFocus={true} + isOpen={this.state.customizingAvatar} + > +
+ +
+
+
); diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap index 3835fa085c26e..ee1eb7c5e9aba 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap @@ -2,14 +2,14 @@ exports[`EnabledFeatures renders as expected 1`] = ` @@ -41,7 +41,7 @@ exports[`EnabledFeatures renders as expected 1`] = ` >

@@ -63,16 +63,16 @@ exports[`EnabledFeatures renders as expected 1`] = `

@@ -89,6 +89,12 @@ exports[`EnabledFeatures renders as expected 1`] = ` Array [ Object { "app": Array [], + "category": Object { + "euiIconType": "logoKibana", + "id": "kibana", + "label": "Kibana", + "order": 1000, + }, "icon": "spacesApp", "id": "feature-1", "name": "Feature 1", @@ -96,6 +102,12 @@ exports[`EnabledFeatures renders as expected 1`] = ` }, Object { "app": Array [], + "category": Object { + "euiIconType": "logoKibana", + "id": "kibana", + "label": "Kibana", + "order": 1000, + }, "icon": "spacesApp", "id": "feature-2", "name": "Feature 2", diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx index 0eed6793ddbe0..4b22b92cfee16 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Space } from '../../../../common/model/space'; -import { SectionPanel } from '../section_panel'; +import { mountWithIntl, nextTick, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { EnabledFeatures } from './enabled_features'; import { KibanaFeatureConfig } from '../../../../../features/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../../../../src/core/public'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { EuiCheckboxProps } from '@elastic/eui'; const features: KibanaFeatureConfig[] = [ { @@ -18,6 +18,7 @@ const features: KibanaFeatureConfig[] = [ name: 'Feature 1', icon: 'spacesApp', app: [], + category: DEFAULT_APP_CATEGORIES.kibana, privileges: null, }, { @@ -25,16 +26,11 @@ const features: KibanaFeatureConfig[] = [ name: 'Feature 2', icon: 'spacesApp', app: [], + category: DEFAULT_APP_CATEGORIES.kibana, privileges: null, }, ]; -const space: Space = { - id: 'my-space', - name: 'my space', - disabledFeatures: ['feature-1', 'feature-2'], -}; - describe('EnabledFeatures', () => { const getUrlForApp = (appId: string) => appId; @@ -43,7 +39,11 @@ describe('EnabledFeatures', () => { shallowWithIntl( { ).toMatchSnapshot(); }); - it('allows all features to be toggled on', () => { + it('allows all features in a category to be toggled on', () => { const changeHandler = jest.fn(); const wrapper = mountWithIntl( ); - // expand section panel - wrapper.find(SectionPanel).find(EuiLink).simulate('click'); - - // Click the "Change all" link - wrapper.find('.spcToggleAllFeatures__changeAllLink').first().simulate('click'); + // Click category-level toggle + const { + onChange = () => { + throw new Error('expected onChange to be defined'); + }, + } = wrapper.find('input#featureCategoryCheckbox_kibana').props() as EuiCheckboxProps; + onChange({ target: { checked: true } } as any); // Ask to show all features - wrapper.find('button[data-test-subj="spc-toggle-all-features-show"]').simulate('click'); + findTestSubject(wrapper, `featureCategoryButton_kibana`).simulate('click'); expect(changeHandler).toBeCalledTimes(1); @@ -81,27 +87,67 @@ describe('EnabledFeatures', () => { expect(updatedSpace.disabledFeatures).toEqual([]); }); - it('allows all features to be toggled off', () => { + it('allows all features in a category to be toggled off', async () => { const changeHandler = jest.fn(); const wrapper = mountWithIntl( ); - // expand section panel - wrapper.find(SectionPanel).find(EuiLink).simulate('click'); + // Click category-level toggle + const { + onChange = () => { + throw new Error('expected onChange to be defined'); + }, + } = wrapper.find('input#featureCategoryCheckbox_kibana').props() as EuiCheckboxProps; + onChange({ target: { checked: false } } as any); + + // Ask to show all features + findTestSubject(wrapper, `featureCategoryButton_kibana`).simulate('click'); + + await nextTick(); + wrapper.update(); + + expect(changeHandler).toBeCalledTimes(1); + + const updatedSpace = changeHandler.mock.calls[0][0]; + + expect(updatedSpace.disabledFeatures).toEqual(['feature-1', 'feature-2']); + }); + + it('allows all features to be toggled off', async () => { + const changeHandler = jest.fn(); + + const wrapper = mountWithIntl( + + ); - // Click the "Change all" link - wrapper.find('.spcToggleAllFeatures__changeAllLink').first().simulate('click'); + // show should not be visible when all features are already visible + expect(findTestSubject(wrapper, 'showAllFeaturesLink')).toHaveLength(0); + findTestSubject(wrapper, 'hideAllFeaturesLink').simulate('click'); - // Ask to hide all features - wrapper.find('button[data-test-subj="spc-toggle-all-features-hide"]').simulate('click'); + await nextTick(); + wrapper.update(); expect(changeHandler).toBeCalledTimes(1); @@ -109,4 +155,109 @@ describe('EnabledFeatures', () => { expect(updatedSpace.disabledFeatures).toEqual(['feature-1', 'feature-2']); }); + + it('allows all features to be toggled on', async () => { + const changeHandler = jest.fn(); + + const wrapper = mountWithIntl( + + ); + + // hide should not be visible when all features are already hidden + expect(findTestSubject(wrapper, 'hideAllFeaturesLink')).toHaveLength(0); + findTestSubject(wrapper, 'showAllFeaturesLink').simulate('click'); + + await nextTick(); + wrapper.update(); + + expect(changeHandler).toBeCalledTimes(1); + + const updatedSpace = changeHandler.mock.calls[0][0]; + + expect(updatedSpace.disabledFeatures).toEqual([]); + }); + + it('displays both show and hide options when a non-zero subset of features are toggled on', async () => { + const wrapper = mountWithIntl( + + ); + expect(findTestSubject(wrapper, 'hideAllFeaturesLink')).toHaveLength(1); + expect(findTestSubject(wrapper, 'showAllFeaturesLink')).toHaveLength(1); + }); + + describe('feature category button', () => { + it(`does not toggle visibility when it contains more than one item`, () => { + const changeHandler = jest.fn(); + const wrapper = mountWithIntl( + + ); + + findTestSubject(wrapper, `featureCategoryButton_kibana`).simulate('click'); + expect(changeHandler).not.toHaveBeenCalled(); + }); + + it('toggles item visibility when the category contains a single item', () => { + const changeHandler = jest.fn(); + const wrapper = mountWithIntl( + + ); + + findTestSubject(wrapper, `featureCategoryButton_management`).simulate('click'); + expect(changeHandler).toBeCalledTimes(1); + + const updatedSpace = changeHandler.mock.calls[0][0]; + + expect(updatedSpace.disabledFeatures).toEqual(['feature-3']); + }); + }); }); diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx index 689bb610d5f38..5e7629c29bbdd 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx @@ -34,8 +34,8 @@ export class EnabledFeatures extends Component { return ( {

@@ -114,7 +114,7 @@ export class EnabledFeatures extends Component { {' '} {details} @@ -135,16 +135,16 @@ export class EnabledFeatures extends Component {

), diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.scss b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.scss new file mode 100644 index 0000000000000..4f73349edac20 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.scss @@ -0,0 +1,4 @@ +.spcFeatureTableAccordionContent { + // Align accordion content with the feature category logo in the accordion's buttonContent + padding-left: $euiSizeXL; +} \ No newline at end of file diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index 9265ca46e3a3a..95ff475ef4e30 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -4,14 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiIcon, EuiInMemoryTable, EuiSwitch, EuiText, IconType } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; + +import { + EuiAccordion, + EuiCheckbox, + EuiCheckboxProps, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiLink, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { AppCategory } from 'kibana/public'; import _ from 'lodash'; -import React, { ChangeEvent, Component } from 'react'; +import React, { ChangeEvent, Component, ReactElement } from 'react'; import { KibanaFeatureConfig } from '../../../../../../plugins/features/public'; import { Space } from '../../../../common/model/space'; -import { ToggleAllFeatures } from './toggle_all_features'; +import { getEnabledFeatures } from '../../lib/feature_utils'; +import './feature_table.scss'; interface Props { space: Partial; @@ -20,15 +35,201 @@ interface Props { } export class FeatureTable extends Component { + private featureCategories: Map = new Map(); + + constructor(props: Props) { + super(props); + // features are static for the lifetime of the page, so this is safe to do here in a non-reactive manner + props.features.forEach((feature) => { + if (!this.featureCategories.has(feature.category.id)) { + this.featureCategories.set(feature.category.id, []); + } + this.featureCategories.get(feature.category.id)!.push(feature); + }); + } + public render() { - const { space, features } = this.props; + const { space } = this.props; + + const accordions: Array<{ order: number; element: ReactElement }> = []; + this.featureCategories.forEach((featuresInCategory) => { + const { category } = featuresInCategory[0]; + + const featureCount = featuresInCategory.length; + const enabledCount = getEnabledFeatures(featuresInCategory, space).length; + + const canExpandCategory = featuresInCategory.length > 1; + + const checkboxProps: EuiCheckboxProps = { + id: `featureCategoryCheckbox_${category.id}`, + indeterminate: enabledCount > 0 && enabledCount < featureCount, + checked: featureCount === enabledCount, + ['aria-label']: i18n.translate( + 'xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel', + { defaultMessage: 'Category toggle' } + ), + onClick: (e) => { + // Clicking the checkbox should not cause the accordion to expand. + // Stopping event propagation ensures this. + e.stopPropagation(); + }, + onChange: (e) => { + this.setFeaturesVisibility( + featuresInCategory.map((f) => f.id), + e.target.checked + ); + }, + }; + + const buttonContent = ( + { + if (!canExpandCategory) { + const isChecked = enabledCount > 0; + this.setFeaturesVisibility( + featuresInCategory.map((f) => f.id), + !isChecked + ); + } + }} + > + + + + {category.euiIconType ? ( + + + + ) : null} + + +

{category.label}

+ + + + ); + + const label: string = i18n.translate('xpack.spaces.management.featureAccordionSwitchLabel', { + defaultMessage: '{enabledCount} / {featureCount} features visible', + values: { + enabledCount, + featureCount, + }, + }); + const extraAction = ( + + ); + + const helpText = this.getCategoryHelpText(category); + + const accordion = ( + +
+ + {helpText && ( + <> + + {helpText} + + + + )} + {featuresInCategory.map((feature) => { + const featureChecked = !( + space.disabledFeatures && space.disabledFeatures.includes(feature.id) + ); + + return ( + + + + + + ); + })} +
+
+ ); + + accordions.push({ + order: category.order ?? Number.MAX_SAFE_INTEGER, + element: accordion, + }); + }); - const items = features.map((feature) => ({ - feature, - space, - })); + accordions.sort((a1, a2) => a1.order - a2.order); - return ; + const featureCount = this.props.features.length; + const enabledCount = getEnabledFeatures(this.props.features, this.props.space).length; + const controls = []; + if (enabledCount < featureCount) { + controls.push( + this.showAll()} data-test-subj="showAllFeaturesLink"> + + {i18n.translate('xpack.spaces.management.selectAllFeaturesLink', { + defaultMessage: 'Select all', + })} + + + ); + } + if (enabledCount > 0) { + controls.push( + this.hideAll()} data-test-subj="hideAllFeaturesLink"> + + {i18n.translate('xpack.spaces.management.deselectAllFeaturesLink', { + defaultMessage: 'Deselect all', + })} + + + ); + } + + return ( +
+ + + + + {i18n.translate('xpack.spaces.management.featureVisibilityTitle', { + defaultMessage: 'Feature visibility', + })} + + + + {controls.map((control, idx) => ( + + {control} + + ))} + + + {accordions.flatMap((a, idx) => [ + a.element, + , + ])} +
+ ); } public onChange = (featureId: string) => (e: ChangeEvent) => { @@ -49,67 +250,41 @@ export class FeatureTable extends Component { this.props.onChange(updatedSpace); }; - private onChangeAll = (visible: boolean) => { + private getAllFeatureIds = () => + [...this.featureCategories.values()].flat().map((feature) => feature.id); + + private hideAll = () => { + this.setFeaturesVisibility(this.getAllFeatureIds(), false); + }; + + private showAll = () => { + this.setFeaturesVisibility(this.getAllFeatureIds(), true); + }; + + private setFeaturesVisibility = (features: string[], visible: boolean) => { const updatedSpace: Partial = { ...this.props.space, }; if (visible) { - updatedSpace.disabledFeatures = []; + updatedSpace.disabledFeatures = (updatedSpace.disabledFeatures ?? []).filter( + (df) => !features.includes(df) + ); } else { - updatedSpace.disabledFeatures = this.props.features.map((feature) => feature.id); + updatedSpace.disabledFeatures = Array.from( + new Set([...(updatedSpace.disabledFeatures ?? []), ...features]) + ); } this.props.onChange(updatedSpace); }; - private getColumns = () => [ - { - field: 'feature', - name: i18n.translate('xpack.spaces.management.enabledSpaceFeaturesFeatureColumnTitle', { - defaultMessage: 'Feature', - }), - render: ( - feature: KibanaFeatureConfig, - _item: { feature: KibanaFeatureConfig; space: Props['space'] } - ) => { - return ( - - -   {feature.name} - - ); - }, - }, - { - field: 'space', - width: '150', - name: ( - - - - - ), - - render: (spaceEntry: Space, record: Record) => { - const checked = !( - spaceEntry.disabledFeatures && spaceEntry.disabledFeatures.includes(record.feature.id) - ); - - return ( - - ); - }, - }, - ]; + private getCategoryHelpText = (category: AppCategory) => { + if (category.id === 'management') { + return i18n.translate('xpack.spaces.management.managementCategoryHelpText', { + defaultMessage: + 'Access to Stack Management is determined by your privileges, and cannot be hidden by Spaces.', + }); + } + }; } diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx index f580720848875..66f5ea87551d3 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiLink, EuiSwitch } from '@elastic/eui'; +import { EuiButton, EuiCheckboxProps } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; import { ManageSpacePage } from './manage_space_page'; -import { SectionPanel } from './section_panel'; import { spacesManagerMock } from '../../spaces_manager/mocks'; import { SpacesManager } from '../../spaces_manager'; import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mocks'; import { featuresPluginMock } from '../../../../features/public/mocks'; import { KibanaFeature } from '../../../../features/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../../../src/core/public'; // To be resolved by EUI team. // https://github.com/elastic/eui/issues/3712 @@ -39,6 +39,7 @@ featuresStart.getFeatures.mockResolvedValue([ name: 'feature 1', icon: 'spacesApp', app: [], + category: DEFAULT_APP_CATEGORIES.kibana, privileges: null, }), ]); @@ -309,16 +310,12 @@ function updateSpace(wrapper: ReactWrapper, updateFeature = true) { } function toggleFeature(wrapper: ReactWrapper) { - const featureSectionButton = wrapper - .find(SectionPanel) - .filter('[data-test-subj="enabled-features-panel"]') - .find(EuiLink); - - featureSectionButton.simulate('click'); - - wrapper.update(); - - wrapper.find(EuiSwitch).find('button').simulate('click'); + const { + onChange = () => { + throw new Error('expected onChange to be defined'); + }, + } = wrapper.find('input#featureCategoryCheckbox_kibana').props() as EuiCheckboxProps; + onChange({ target: { checked: false } } as any); wrapper.update(); } diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index 5338710b7c8a4..6943e27501554 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -177,11 +177,16 @@ export class ManageSpacePage extends Component { }; public getFormHeading = () => ( - -

- {this.getTitle()} -

-
+ + + +

{this.getTitle()}

+
+
+ + + +
); public getTitle = () => { diff --git a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx index 2d1ec727b3348..d9ad63c30adde 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiIcon } from '@elastic/eui'; +import { EuiBadge } from '@elastic/eui'; import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ReservedSpaceBadge } from './reserved_space_badge'; @@ -24,7 +24,7 @@ const unreservedSpace = { test('it renders without crashing', () => { const wrapper = shallowWithIntl(); - expect(wrapper.find(EuiIcon)).toHaveLength(1); + expect(wrapper.find(EuiBadge)).toHaveLength(1); }); test('it renders nothing for an unreserved space', () => { diff --git a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx index 38bf351902096..f3a2273d90e8c 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { isReservedSpace } from '../../../common'; import { Space } from '../../../common/model/space'; @@ -28,7 +28,9 @@ export const ReservedSpaceBadge = (props: Props) => { /> } > - + + Reserved space + ); } diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx index fe4bdc865094f..c1d19eb06c2e7 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx @@ -47,6 +47,7 @@ featuresStart.getFeatures.mockResolvedValue([ name: 'feature 1', icon: 'spacesApp', app: [], + category: { id: 'foo', label: 'foo' }, privileges: null, }), ]); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index 1e8520a2617dd..e345657a785c1 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -88,7 +88,11 @@ describe('spacesManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Spaces' }]); expect(container).toMatchInlineSnapshot(`
- Spaces Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"securityEnabled":true} +
`); @@ -107,7 +111,11 @@ describe('spacesManagementApp', () => { ]); expect(container).toMatchInlineSnapshot(`
- Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"securityEnabled":true} +
`); @@ -128,7 +136,11 @@ describe('spacesManagementApp', () => { ]); expect(container).toMatchInlineSnapshot(`
- Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"securityEnabled":true} +
`); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 5b8b993d96adc..a328c50af4e7a 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -9,6 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Router, Route, Switch, useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; +import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; import { SecurityLicense } from '../../../security/public'; import { RegisterManagementAppArgs } from '../../../../../src/plugins/management/public'; import { PluginsStart } from '../plugin'; @@ -32,6 +33,7 @@ export const spacesManagementApp = Object.freeze({ title: i18n.translate('xpack.spaces.displayName', { defaultMessage: 'Spaces', }), + async mount({ element, setBreadcrumbs, history }) { const [ { notifications, i18n: i18nStart, application }, @@ -114,19 +116,21 @@ export const spacesManagementApp = Object.freeze({ render( - - - - - - - - - - - - - + + + + + + + + + + + + + + + , element ); diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts index 9b69540007e5f..0ff75717c8b65 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; import { TIMEOUT } from './constants'; /** @@ -14,7 +14,7 @@ import { TIMEOUT } from './constants'; * * Like any X-Pack related API, X-Pack must installed for this to work. */ -export function getXPackUsage(callCluster: CallCluster) { +export function getXPackUsage(callCluster: LegacyAPICaller) { return callCluster('transport.request', { method: 'GET', path: '/_xpack/usage', diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index fa4f8a7e09690..c4adb9f1f49de 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -8,7 +8,7 @@ import React, { Fragment, FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { dictionaryToArray } from '../../../../../../common/types/common'; @@ -132,23 +132,21 @@ export const StepDefineSummary: FC = ({ - - - +

); diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index c02bc06ad6060..3d2018eb5801f 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -10,8 +10,8 @@ import { RequestHandler, RequestHandlerContext, SavedObjectsClientContract, + LegacyAPICaller, } from 'kibana/server'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; import { TRANSFORM_STATE } from '../../../common/constants'; @@ -394,7 +394,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { const getTransforms = async ( options: { transformId?: string }, - callAsCurrentUser: CallCluster + callAsCurrentUser: LegacyAPICaller ): Promise => { return await callAsCurrentUser('transform.getTransforms', options); }; @@ -570,7 +570,7 @@ const startTransformsHandler: RequestHandler< async function startTransforms( transformsInfo: StartTransformsRequestSchema, - callAsCurrentUser: CallCluster + callAsCurrentUser: LegacyAPICaller ) { const results: StartTransformsResponseSchema = {}; @@ -612,7 +612,7 @@ const stopTransformsHandler: RequestHandler< async function stopTransforms( transformsInfo: StopTransformsRequestSchema, - callAsCurrentUser: CallCluster + callAsCurrentUser: LegacyAPICaller ) { const results: StopTransformsResponseSchema = {}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 916dab88d8b73..1223b3f42f8cd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7978,7 +7978,6 @@ "xpack.indexLifecycleMgmt.appTitle": "インデックスライフサイクルポリシー", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "複製の数", - "xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText": "デフォルトで、複製の数は同じままになります。", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "キャンセル", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "削除", "xpack.indexLifecycleMgmt.confirmDelete.errorMessage": "ポリシー {policyName} の削除中にエラーが発生しました", @@ -7986,7 +7985,6 @@ "xpack.indexLifecycleMgmt.confirmDelete.title": "ポリシー「{name}」が削除されました", "xpack.indexLifecycleMgmt.confirmDelete.undoneWarning": "削除されたポリシーは復元できません。", "xpack.indexLifecycleMgmt.editPolicy.cancelButton": "キャンセル", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateWarmPhaseSwitchLabel": "コールドフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "インデックスへのクエリの頻度を減らすことで、大幅に性能が低いハードウェアにシャードを割り当てることができます。クエリが遅いため、複製の数を減らすことができます。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "コールドフェーズ", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "凍結されたインデックスはクラスターにほとんどオーバーヘッドがなく、書き込みオペレーションがブロックされます。凍結されたインデックスは検索できますが、クエリが遅くなります。", @@ -8032,7 +8030,6 @@ "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大インデックスサイズが必要です。", "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名前", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "シャードの割当をコントロールするノード属性を選択", - "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "ノード属性なしではシャードの割り当てをコントロールできません。", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml でノード属性が構成されていません", "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字が必要です。", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "コールドフェーズのタイミング", @@ -8188,7 +8185,6 @@ "xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "プライマリシャードの数", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "レプリカの数", "xpack.indexLifecycleMgmt.warmPhase.numberOfSegmentsLabel": "セグメントの数", - "xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText": "デフォルトで、レプリカの数は同じままになります。", "xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "インデックスを縮小", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", @@ -8681,8 +8677,7 @@ "xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "タイミング", "xpack.infra.metrics.alertFlyout.filterHelpText": "KQL式を使用して、アラートトリガーの範囲を制限します。", "xpack.infra.metrics.alertFlyout.filterLabel": "フィルター(任意)", - "xpack.infra.metrics.alertFlyout.firedTime": "時間", - "xpack.infra.metrics.alertFlyout.firedTimes": "回数", + "xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 時間} other {# 回数}}", "xpack.infra.metrics.alertFlyout.hourLabel": "時間", "xpack.infra.metrics.alertFlyout.lastDayLabel": "昨日", "xpack.infra.metrics.alertFlyout.lastHourLabel": "過去1時間", @@ -13941,8 +13936,6 @@ "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました。{encryptionKey}が設定されていることを確認してこのレポートを再生成してください。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "ジョブヘッダーがありません", "xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana の高度な設定「{dateFormatTimezone}」が「ブラウザー」に設定されていますあいまいさを避けるために日付は UTC 形式に変換されます。", - "xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました{encryptionKey} が設定されていることを確認してこのレポートを再生成してください。{err}", - "xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage": "ジョブヘッダーがありません", "xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues": "CSVには、値がエスケープされた式が含まれる場合があります", "xpack.reporting.exportTypes.csv.hitIterator.expectedHitsErrorMessage": "次の Elasticsearch からの応答で期待される {hits}: {response}", "xpack.reporting.exportTypes.csv.hitIterator.expectedScrollIdErrorMessage": "次の Elasticsearch からの応答で期待される {scrollId}: {response}", @@ -17543,13 +17536,10 @@ "xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(表示されているすべての機能)", "xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "機能の表示をカスタマイズ", "xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "このスペースでどの機能が表示されるかを管理します。", - "xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "セキュアなアクセスをご希望の場合は、{rolesLink} にアクセスしてください。", "xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(表示されている機能がありません)", "xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "この機能は UI で非表示になっていますが、無効ではありません。", "xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "ロール", "xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "({featureCount} 件中 {enabledCount} 件の機能を表示中)", - "xpack.spaces.management.enabledSpaceFeaturesEnabledColumnTitle": "表示しますか?", - "xpack.spaces.management.enabledSpaceFeaturesFeatureColumnTitle": "機能", "xpack.spaces.management.hideAllFeaturesText": "すべて非表示", "xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "アバター", "xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "素晴らしいスペース", @@ -17557,10 +17547,8 @@ "xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "クリックしてこのスペースのアバターをカスタマイズします", "xpack.spaces.management.manageSpacePage.createSpaceButton": "スペースを作成", "xpack.spaces.management.manageSpacePage.createSpaceTitle": "スペースの作成", - "xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "スペースに名前を付けてアバターをカスタマイズします", "xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "URL 識別子に注意してください。スペースの作成後に変更することはできません。", "xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 識別子は変更できません。", - "xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "スペースのカスタマイズ", "xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "表示される機能のカスタマイズ", "xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "スペースの読み込み中にエラーが発生: {message}", "xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "スペースの保存中にエラーが発生: {message}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ffaf281487fd0..f1bab383d53f5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7982,7 +7982,6 @@ "xpack.indexLifecycleMgmt.appTitle": "索引生命周期策略", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引", "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "副本分片数目", - "xpack.indexLifecycleMgmt.coldPhase.replicaCountHelpText": "默认情况下,副本分片数目仍一样。", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "取消", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "删除", "xpack.indexLifecycleMgmt.confirmDelete.errorMessage": "删除策略 {policyName} 时出错", @@ -7990,7 +7989,6 @@ "xpack.indexLifecycleMgmt.confirmDelete.title": "删除策略“{name}”", "xpack.indexLifecycleMgmt.confirmDelete.undoneWarning": "无法恢复删除的策略。", "xpack.indexLifecycleMgmt.editPolicy.cancelButton": "取消", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateWarmPhaseSwitchLabel": "激活冷阶段", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescriptionText": "您查询自己索引的频率较低,因此您可以在效率较低的硬件上分配分片。因为您的查询较为缓慢,所以您可以减少副本分片数目。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseLabel": "冷阶段", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "冻结的索引在集群上有很少的开销,已被阻止进行写操作。您可以搜索冻结的索引,但查询应会较慢。", @@ -8036,7 +8034,6 @@ "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大索引大小必填。", "xpack.indexLifecycleMgmt.editPolicy.nameLabel": "名称", "xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "选择节点属性来控制分片分配", - "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "没有节点属性,将无法控制分片分配。", "xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml 中未配置任何节点属性", "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字必填。", "xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "冷阶段计时", @@ -8192,7 +8189,6 @@ "xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "主分片数目", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "副本分片数目", "xpack.indexLifecycleMgmt.warmPhase.numberOfSegmentsLabel": "段数目", - "xpack.indexLifecycleMgmt.warmPhase.replicaCountHelpText": "默认情况下,副本分片数目仍一样。", "xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "缩小索引", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", @@ -8687,8 +8683,7 @@ "xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "当", "xpack.infra.metrics.alertFlyout.filterHelpText": "使用 KQL 表达式限制告警触发器的范围。", "xpack.infra.metrics.alertFlyout.filterLabel": "筛选(可选)", - "xpack.infra.metrics.alertFlyout.firedTime": "次", - "xpack.infra.metrics.alertFlyout.firedTimes": "次", + "xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 次} other {# 次}}", "xpack.infra.metrics.alertFlyout.hourLabel": "小时", "xpack.infra.metrics.alertFlyout.lastDayLabel": "昨天", "xpack.infra.metrics.alertFlyout.lastHourLabel": "上一小时", @@ -13950,8 +13945,6 @@ "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "作业标头缺失", "xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana 高级设置“{dateFormatTimezone}”已设置为“浏览器”。日期将格式化为 UTC 以避免混淆。", - "xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}", - "xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage": "作业标头缺失", "xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues": "CSV 可能包含值已转义的公式", "xpack.reporting.exportTypes.csv.hitIterator.expectedHitsErrorMessage": "在以下 Elasticsearch 响应中预期 {hits}:{response}", "xpack.reporting.exportTypes.csv.hitIterator.expectedScrollIdErrorMessage": "在以下 Elasticsearch 响应中预期 {scrollId}:{response}", @@ -17553,13 +17546,10 @@ "xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(所有可见功能)", "xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "定制功能显示", "xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "控制哪些功能在此工作区中可见。", - "xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "想保护访问?前往 {rolesLink}。", "xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(没有可见功能)", "xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "该功能在 UI 中已隐藏,但未禁用。", "xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "角色", "xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "({enabledCount} / {featureCount} 个功能可见)", - "xpack.spaces.management.enabledSpaceFeaturesEnabledColumnTitle": "显示?", - "xpack.spaces.management.enabledSpaceFeaturesFeatureColumnTitle": "功能", "xpack.spaces.management.hideAllFeaturesText": "全部隐藏", "xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "头像", "xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "超卓的空间", @@ -17567,10 +17557,8 @@ "xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "单击可定制此工作区头像", "xpack.spaces.management.manageSpacePage.createSpaceButton": "创建工作区", "xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建一个空间", - "xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "命名您的工作区并定制其头像。", "xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "记下 URL 标识符。创建工作区后,将不能更改它。", "xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 标识符无法更改。", - "xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "定制您的工作区", "xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "定制可见功能", "xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "加载空间时出错:{message}", "xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "保存空间时出错:{message}", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index c0674e6c4a5f7..c69c33c0fe22e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -244,10 +244,11 @@ export const AlertForm = ({ ) : null} {AlertParamsExpressionComponent ? ( - + }> { alertParams: AlertParamsType; alertInterval: string; + alertThrottle: string; setAlertParams: (property: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; errors: IErrorObject; diff --git a/x-pack/plugins/ui_actions_enhanced/.eslintrc.json b/x-pack/plugins/ui_actions_enhanced/.eslintrc.json new file mode 100644 index 0000000000000..2aab6c2d9093b --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/consistent-type-definitions": 0 + } +} diff --git a/x-pack/plugins/ui_actions_enhanced/common/types.ts b/x-pack/plugins/ui_actions_enhanced/common/types.ts new file mode 100644 index 0000000000000..1150f4f823e8e --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/common/types.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; + +export type BaseActionConfig = SerializableState; + +export type SerializedAction = { + readonly factoryId: string; + readonly name: string; + readonly config: Config; +}; + +/** + * Serialized representation of a triggers-action pair, used to persist in storage. + */ +export type SerializedEvent = { + eventId: string; + triggers: string[]; + action: SerializedAction; +}; + +export type DynamicActionsState = { + events: SerializedEvent[]; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/kibana.json b/x-pack/plugins/ui_actions_enhanced/kibana.json index 108c66505f25c..5435019f216f2 100644 --- a/x-pack/plugins/ui_actions_enhanced/kibana.json +++ b/x-pack/plugins/ui_actions_enhanced/kibana.json @@ -7,7 +7,7 @@ "uiActions", "licensing" ], - "server": false, + "server": true, "ui": true, "requiredBundles": [ "kibanaUtils", diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx index 9cc64defc1795..fcea8caf9090e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx @@ -82,3 +82,18 @@ test('If not enough license, button is disabled', () => { expect(screen.getByText(/Go to URL/i)).toBeDisabled(); }); + +test('if action is beta, beta badge is shown', () => { + const betaUrl = new ActionFactory( + { + ...urlDrilldownActionFactory, + isBeta: true, + }, + { + getLicense: () => licensingMock.createLicense(), + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, + } + ); + const screen = render(); + expect(screen.getByText(/Beta/i)).toBeVisible(); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index 16d0250c5721e..ca7f6af4f7a37 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx @@ -19,16 +19,19 @@ import { EuiTextColor, EuiTitle, EuiLink, + EuiBetaBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { + txtBetaActionFactoryLabel, + txtBetaActionFactoryTooltip, txtChangeButton, txtTriggerPickerHelpText, txtTriggerPickerLabel, txtTriggerPickerHelpTooltip, } from './i18n'; import './action_wizard.scss'; -import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions'; +import { ActionFactory, BaseActionConfig, BaseActionFactoryContext } from '../../dynamic_actions'; import { Trigger, TriggerId } from '../../../../../../src/plugins/ui_actions/public'; export interface ActionWizardProps< @@ -54,12 +57,12 @@ export interface ActionWizardProps< /** * current config for currently selected action factory */ - config?: object; + config?: BaseActionConfig; /** * config changed */ - onConfigChange: (config: object) => void; + onConfigChange: (config: BaseActionConfig) => void; /** * Context will be passed into ActionFactory's methods @@ -216,9 +219,9 @@ interface SelectedActionFactoryProps< ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext > { actionFactory: ActionFactory; - config: object; + config: BaseActionConfig; context: ActionFactoryContext; - onConfigChange: (config: object) => void; + onConfigChange: (config: BaseActionConfig) => void; showDeselect: boolean; onDeselect: () => void; allTriggers: TriggerId[]; @@ -255,7 +258,15 @@ const SelectedActionFactory: React.FC = ({ )} -

{actionFactory.getDisplayName(context)}

+

+ {actionFactory.getDisplayName(context)}{' '} + {actionFactory.isBeta && ( + + )} +

{showDeselect && ( @@ -350,6 +361,10 @@ const ActionFactorySelector: React.FC = ({ data-test-subj={`${TEST_SUBJ_ACTION_FACTORY_ITEM}-${actionFactory.id}`} onClick={() => onActionFactorySelected(actionFactory)} disabled={!actionFactory.isCompatibleLicense()} + betaBadgeLabel={actionFactory.isBeta ? txtBetaActionFactoryLabel : undefined} + betaBadgeTooltipContent={ + actionFactory.isBeta ? txtBetaActionFactoryTooltip : undefined + } > {actionFactory.getIconType(context) && ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts index f494ecfb51f32..43a3bd01daf37 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts @@ -33,3 +33,17 @@ export const txtTriggerPickerHelpTooltip = i18n.translate( defaultMessage: 'Determines when the drilldown appears in context menu', } ); + +export const txtBetaActionFactoryLabel = i18n.translate( + 'xpack.uiActionsEnhanced.components.actionWizard.betaActionLabel', + { + defaultMessage: `Beta`, + } +); + +export const txtBetaActionFactoryTooltip = i18n.translate( + 'xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip', + { + defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting any bugs or providing other feedback.`, + } +); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx index 71286e9a59c06..af930bfba6b8b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { EuiFieldText, EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; import { ActionWizard } from './action_wizard'; -import { ActionFactory, ActionFactoryDefinition } from '../../dynamic_actions'; +import { ActionFactory, ActionFactoryDefinition, BaseActionConfig } from '../../dynamic_actions'; import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public'; import { licensingMock } from '../../../../licensing/public/mocks'; import { @@ -19,18 +19,16 @@ import { VALUE_CLICK_TRIGGER, } from '../../../../../../src/plugins/ui_actions/public'; -type ActionBaseConfig = object; - export const dashboards = [ { id: 'dashboard1', title: 'Dashboard 1' }, { id: 'dashboard2', title: 'Dashboard 2' }, ]; -interface DashboardDrilldownConfig { +type DashboardDrilldownConfig = { dashboardId?: string; useCurrentFilters: boolean; useCurrentDateRange: boolean; -} +}; function DashboardDrilldownCollectConfig(props: CollectConfigProps) { const config = props.config ?? { @@ -121,10 +119,11 @@ export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactor getFeatureUsageStart: () => licensingMock.createStart().featureUsage, }); -interface UrlDrilldownConfig { +type UrlDrilldownConfig = { url: string; openInNewTab: boolean; -} +}; + function UrlDrilldownCollectConfig(props: CollectConfigProps) { const config = props.config ?? { url: '', @@ -182,6 +181,10 @@ export const urlFactory = new ActionFactory(urlDrilldownActionFactory, { getFeatureUsageStart: () => licensingMock.createStart().featureUsage, }); +export const mockActionFactories: ActionFactory[] = ([dashboardFactory, urlFactory] as Array< + ActionFactory +>) as ActionFactory[]; + export const mockSupportedTriggers: TriggerId[] = [ VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, @@ -210,7 +213,7 @@ export const mockGetTriggerInfo = (triggerId: TriggerId): Trigger => { export function Demo({ actionFactories }: { actionFactories: Array> }) { const [state, setState] = useState<{ currentActionFactory?: ActionFactory; - config?: ActionBaseConfig; + config?: BaseActionConfig; selectedTriggers?: TriggerId[]; }>({}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx index f7284539ab2fe..daa56354289cf 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx @@ -8,14 +8,13 @@ import * as React from 'react'; import { EuiFlyout } from '@elastic/eui'; import { storiesOf } from '@storybook/react'; import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns'; -import { dashboardFactory, urlFactory } from '../../../components/action_wizard/test_data'; +import { mockActionFactories } from '../../../components/action_wizard/test_data'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { StubBrowserStorage } from '../../../../../../../src/test_utils/public/stub_browser_storage'; import { mockDynamicActionManager } from './test_data'; -import { ActionFactory } from '../../../dynamic_actions'; const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ - actionFactories: [dashboardFactory as ActionFactory, urlFactory as ActionFactory], + actionFactories: mockActionFactories, storage: new Storage(new StubBrowserStorage()), toastService: { addError: (...args: any[]) => { diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index 2412cdd51748c..c4b07fa05c3c1 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -8,10 +8,9 @@ import React from 'react'; import { cleanup, fireEvent, render, wait } from '@testing-library/react/pure'; import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns'; import { - dashboardFactory, mockGetTriggerInfo, mockSupportedTriggers, - urlFactory, + mockActionFactories, } from '../../../components/action_wizard/test_data'; import { StubBrowserStorage } from '../../../../../../../src/test_utils/public/stub_browser_storage'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; @@ -21,12 +20,11 @@ import { WELCOME_MESSAGE_TEST_SUBJ } from '../drilldown_hello_bar'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { NotificationsStart } from 'kibana/public'; import { toastDrilldownsCRUDError } from './i18n'; -import { ActionFactory } from '../../../dynamic_actions'; const storage = new Storage(new StubBrowserStorage()); const toasts = coreMock.createStart().notifications.toasts; const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ - actionFactories: [dashboardFactory as ActionFactory, urlFactory as ActionFactory], + actionFactories: mockActionFactories, storage: new Storage(new StubBrowserStorage()), toastService: toasts, getTrigger: mockGetTriggerInfo, diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 6f9eccde8bdb0..28a0990cf7526 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -25,6 +25,7 @@ import { } from './i18n'; import { ActionFactory, + BaseActionConfig, BaseActionFactoryContext, DynamicActionManager, SerializedAction, @@ -127,7 +128,7 @@ export function createFlyoutManageDrilldowns({ return { actionFactory: allActionFactoriesById[drilldownToEdit.action.factoryId], - actionConfig: drilldownToEdit.action.config as object, + actionConfig: drilldownToEdit.action.config as BaseActionConfig, name: drilldownToEdit.action.name, selectedTriggers: (drilldownToEdit.triggers ?? []) as TriggerId[], }; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts index 58c36e36481b8..78eec05eb2d0b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts @@ -60,7 +60,7 @@ class MockDynamicActionManager implements PublicMethodsOf async updateEvent( eventId: string, - action: UiActionsEnhancedSerializedAction, + action: UiActionsEnhancedSerializedAction, triggers: Array ) { const state = this.state.get(); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx index 8f73c2b3b3cc9..2f5f7760d40bd 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx @@ -8,8 +8,7 @@ import * as React from 'react'; import { EuiFlyout } from '@elastic/eui'; import { storiesOf } from '@storybook/react'; import { FlyoutDrilldownWizard } from './index'; -import { dashboardFactory, urlFactory } from '../../../components/action_wizard/test_data'; -import { ActionFactory } from '../../../dynamic_actions'; +import { mockActionFactories } from '../../../components/action_wizard/test_data'; import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; const otherProps = { @@ -24,23 +23,12 @@ const otherProps = { storiesOf('components/FlyoutDrilldownWizard', module) .add('default', () => { - return ( - - ); + return ; }) .add('open in flyout - create', () => { return ( {}}> - + ); }) @@ -48,13 +36,10 @@ storiesOf('components/FlyoutDrilldownWizard', module) return ( {}}> {}}> { +export interface DrilldownWizardConfig { name: string; actionFactory?: ActionFactory; actionConfig?: ActionConfig; @@ -28,7 +32,7 @@ export interface DrilldownWizardConfig { } export interface FlyoutDrilldownWizardProps< - CurrentActionConfig extends object = object, + CurrentActionConfig extends BaseActionConfig = BaseActionConfig, ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext > { drilldownActionFactories: ActionFactory[]; @@ -71,7 +75,7 @@ function useWizardConfigState( DrilldownWizardConfig, { setName: (name: string) => void; - setActionConfig: (actionConfig: object) => void; + setActionConfig: (actionConfig: BaseActionConfig) => void; setActionFactory: (actionFactory?: ActionFactory) => void; setSelectedTriggers: (triggers?: TriggerId[]) => void; } @@ -100,7 +104,7 @@ function useWizardConfigState( name, }); }, - setActionConfig: (actionConfig: object) => { + setActionConfig: (actionConfig: BaseActionConfig) => { setWizardConfig({ ...wizardConfig, actionConfig, @@ -108,12 +112,12 @@ function useWizardConfigState( }, setActionFactory: (actionFactory?: ActionFactory) => { if (actionFactory) { + const actionConfig = (actionConfigCache[actionFactory.id] ?? + actionFactory.createConfig(actionFactoryContext)) as BaseActionConfig; setWizardConfig({ ...wizardConfig, actionFactory, - actionConfig: - actionConfigCache[actionFactory.id] ?? - actionFactory.createConfig(actionFactoryContext), + actionConfig, selectedTriggers: [], }); } else { @@ -141,7 +145,9 @@ function useWizardConfigState( ]; } -export function FlyoutDrilldownWizard({ +export function FlyoutDrilldownWizard< + CurrentActionConfig extends BaseActionConfig = BaseActionConfig +>({ onClose, onBack, onSubmit = () => {}, diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx index d7f94a52088b7..45655c2634fe7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -8,7 +8,11 @@ import React from 'react'; import { EuiFieldText, EuiForm, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n'; -import { ActionFactory, BaseActionFactoryContext } from '../../../dynamic_actions'; +import { + ActionFactory, + BaseActionConfig, + BaseActionFactoryContext, +} from '../../../dynamic_actions'; import { ActionWizard } from '../../../components/action_wizard'; import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; @@ -26,8 +30,8 @@ export interface FormDrilldownWizardProps< onActionFactoryChange?: (actionFactory?: ActionFactory) => void; actionFactoryContext: ActionFactoryContext; - actionConfig?: object; - onActionConfigChange?: (config: object) => void; + actionConfig?: BaseActionConfig; + onActionConfigChange?: (config: BaseActionConfig) => void; actionFactories?: ActionFactory[]; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index 8faccc088a327..b55b4b87ebccd 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -4,10 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionFactoryDefinition, BaseActionFactoryContext } from '../dynamic_actions'; +import { + ActionFactoryDefinition, + BaseActionConfig, + BaseActionFactoryContext, + SerializedEvent, +} from '../dynamic_actions'; import { LicenseType } from '../../../licensing/public'; import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public'; import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public'; +import { PersistableStateDefinition } from '../../../../../src/plugins/kibana_utils/common'; /** * This is a convenience interface to register a drilldown. Drilldown has @@ -24,18 +30,24 @@ import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/pu */ export interface DrilldownDefinition< - Config extends object = object, + Config extends BaseActionConfig = BaseActionConfig, SupportedTriggers extends TriggerId = TriggerId, FactoryContext extends BaseActionFactoryContext = { triggers: SupportedTriggers[]; }, ExecutionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] -> { +> extends PersistableStateDefinition { /** * Globally unique identifier for this drilldown. */ id: string; + /** + * Is this action factory not GA? + * Adds a beta badge on a list item representing this ActionFactory + */ + readonly isBeta?: boolean; + /** * Minimal license level * Empty means no restrictions diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts index 78f7218dce22e..500ef21b61dc4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts @@ -19,7 +19,8 @@ export const txtUrlTemplatePlaceholder = i18n.translate( export const txtUrlPreviewHelpText = i18n.translate( 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText', { - defaultMessage: 'Please note that \\{\\{event.*\\}\\} variables replaced by dummy values.', + defaultMessage: + 'Please note that in preview \\{\\{event.*\\}\\} variables are substituted with dummy values.', } ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts index 31c7481c9d63e..fb7d96aaf8325 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface UrlDrilldownConfig { +export type UrlDrilldownConfig = { url: { format?: 'handlebars_v1'; template: string }; openInNewTab: boolean; -} +}; /** * URL drilldown has 3 sources for variables: global, context and event variables diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts index 032a4a63fe2e9..66a876bdbab85 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts @@ -114,3 +114,15 @@ describe('License & ActionFactory', () => { }); }); }); + +describe('isBeta', () => { + test('false by default', async () => { + const factory = createActionFactory(); + expect(factory.isBeta).toBe(false); + }); + + test('true', async () => { + const factory = createActionFactory({ isBeta: true }); + expect(factory.isBeta).toBe(true); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 35a82adf9896d..57c8733ed44fc 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -12,9 +12,16 @@ import { } from '../../../../../src/plugins/ui_actions/public'; import { ActionFactoryDefinition } from './action_factory_definition'; import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; -import { BaseActionFactoryContext, SerializedAction } from './types'; +import { + BaseActionConfig, + BaseActionFactoryContext, + SerializedAction, + SerializedEvent, +} from './types'; import { ILicense, LicensingPluginStart } from '../../../licensing/public'; import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; +import { SavedObjectReference } from '../../../../../src/core/types'; +import { PersistableState } from '../../../../../src/plugins/kibana_utils/common'; export interface ActionFactoryDeps { readonly getLicense: () => ILicense; @@ -22,13 +29,16 @@ export interface ActionFactoryDeps { } export class ActionFactory< - Config extends object = object, + Config extends BaseActionConfig = BaseActionConfig, SupportedTriggers extends TriggerId = TriggerId, FactoryContext extends BaseActionFactoryContext = { triggers: SupportedTriggers[]; }, ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] -> implements Omit, 'getHref'>, Configurable { +> implements + Omit, 'getHref'>, + Configurable, + PersistableState { constructor( protected readonly def: ActionFactoryDefinition< Config, @@ -46,6 +56,7 @@ export class ActionFactory< } public readonly id = this.def.id; + public readonly isBeta = this.def.isBeta ?? false; public readonly minimalLicense = this.def.minimalLicense; public readonly licenseFeatureName = this.def.licenseFeatureName; public readonly order = this.def.order || 0; @@ -120,4 +131,16 @@ export class ActionFactory< ); }); } + + public telemetry(state: SerializedEvent, telemetryData: Record) { + return this.def.telemetry ? this.def.telemetry(state, telemetryData) : {}; + } + + public extract(state: SerializedEvent) { + return this.def.extract ? this.def.extract(state) : { state, references: [] }; + } + + public inject(state: SerializedEvent, references: SavedObjectReference[]) { + return this.def.inject ? this.def.inject(state, references) : state; + } } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts index 91b8c8ec1e5ef..b4df1f827a2a3 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts @@ -5,7 +5,12 @@ */ import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; -import { BaseActionFactoryContext, SerializedAction } from './types'; +import { + BaseActionConfig, + BaseActionFactoryContext, + SerializedAction, + SerializedEvent, +} from './types'; import { LicenseType } from '../../../licensing/public'; import { TriggerContextMapping, @@ -13,19 +18,21 @@ import { UiActionsActionDefinition as ActionDefinition, UiActionsPresentable as Presentable, } from '../../../../../src/plugins/ui_actions/public'; +import { PersistableStateDefinition } from '../../../../../src/plugins/kibana_utils/common'; /** * This is a convenience interface for registering new action factories. */ export interface ActionFactoryDefinition< - Config extends object = object, + Config extends BaseActionConfig = BaseActionConfig, SupportedTriggers extends TriggerId = TriggerId, FactoryContext extends BaseActionFactoryContext = { triggers: SupportedTriggers[]; }, ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] > extends Partial, 'getHref'>>, - Configurable { + Configurable, + PersistableStateDefinition { /** * Unique ID of the action factory. This ID is used to identify this action * factory in the registry as well as to construct actions of this type and @@ -46,6 +53,12 @@ export interface ActionFactoryDefinition< */ licenseFeatureName?: string; + /** + * Is this action factory not GA? + * Adds a beta badge on a list item representing this ActionFactory + */ + readonly isBeta?: boolean; + /** * This method should return a definition of a new action, normally used to * register it in `ui_actions` registry. diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts new file mode 100644 index 0000000000000..7cac49624bfdd --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EnhancementRegistryDefinition } from '../../../../../src/plugins/embeddable/public'; +import { SavedObjectReference } from '../../../../../src/core/types'; +import { SerializableState } from '../../../../../src/plugins/kibana_utils/common'; +import { DynamicActionsState } from '../../../ui_actions_enhanced/public'; +import { UiActionsServiceEnhancements } from '../services'; + +export const dynamicActionEnhancement = ( + uiActionsEnhanced: UiActionsServiceEnhancements +): EnhancementRegistryDefinition => { + return { + id: 'dynamicActions', + telemetry: (state: SerializableState, telemetryData: Record) => { + return uiActionsEnhanced.telemetry(state as DynamicActionsState, telemetryData); + }, + extract: (state: SerializableState) => { + return uiActionsEnhanced.extract(state as DynamicActionsState); + }, + inject: (state: SerializableState, references: SavedObjectReference[]) => { + return uiActionsEnhanced.inject(state as DynamicActionsState, references); + }, + } as EnhancementRegistryDefinition; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index 39d9dfeca2fd6..83232bbce1ba7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -250,7 +250,7 @@ describe('DynamicActionManager', () => { uiActions.registerActionFactory(actionFactoryDefinition1); await manager.start(); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition1.id, name: 'foo', config: {}, @@ -277,7 +277,7 @@ describe('DynamicActionManager', () => { test('adds event to UI state', async () => { const { manager, uiActions } = setup([]); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition1.id, name: 'foo', config: {}, @@ -296,7 +296,7 @@ describe('DynamicActionManager', () => { test('optimistically adds event to UI state', async () => { const { manager, uiActions } = setup([]); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition1.id, name: 'foo', config: {}, @@ -319,7 +319,7 @@ describe('DynamicActionManager', () => { test('instantiates event in actions service', async () => { const { manager, uiActions, actions } = setup([]); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition1.id, name: 'foo', config: {}, @@ -348,7 +348,7 @@ describe('DynamicActionManager', () => { uiActions.registerActionFactory(actionFactoryDefinition1); await manager.start(); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition1.id, name: 'foo', config: {}, @@ -361,7 +361,7 @@ describe('DynamicActionManager', () => { test('does not add even to UI state', async () => { const { manager, storage, uiActions } = setup([]); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition1.id, name: 'foo', config: {}, @@ -380,7 +380,7 @@ describe('DynamicActionManager', () => { test('optimistically adds event to UI state and then removes it', async () => { const { manager, storage, uiActions } = setup([]); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition1.id, name: 'foo', config: {}, @@ -406,7 +406,7 @@ describe('DynamicActionManager', () => { test('does not instantiate event in actions service', async () => { const { manager, storage, uiActions, actions } = setup([]); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition1.id, name: 'foo', config: {}, @@ -432,7 +432,7 @@ describe('DynamicActionManager', () => { uiActions.registerActionFactory(actionFactoryDefinition1); await manager.start(); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition1.id, name: 'foo', config: {}, @@ -457,7 +457,7 @@ describe('DynamicActionManager', () => { expect(registeredAction1.getDisplayName()).toBe('Action 3'); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition2.id, name: 'foo', config: {}, @@ -479,7 +479,7 @@ describe('DynamicActionManager', () => { uiActions.registerActionFactory(actionFactoryDefinition2); await manager.start(); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition2.id, name: 'foo', config: {}, @@ -505,7 +505,7 @@ describe('DynamicActionManager', () => { uiActions.registerActionFactory(actionFactoryDefinition2); await manager.start(); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition2.id, name: 'foo', config: {}, @@ -524,7 +524,7 @@ describe('DynamicActionManager', () => { uiActions.registerActionFactory(actionFactoryDefinition2); await manager.start(); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition2.id, name: 'foo', config: {}, @@ -552,7 +552,7 @@ describe('DynamicActionManager', () => { uiActions.registerActionFactory(actionFactoryDefinition2); await manager.start(); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition2.id, name: 'foo', config: {}, @@ -580,7 +580,7 @@ describe('DynamicActionManager', () => { expect(registeredAction1.getDisplayName()).toBe('Action 3'); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition2.id, name: 'foo', config: {}, @@ -604,7 +604,7 @@ describe('DynamicActionManager', () => { uiActions.registerActionFactory(actionFactoryDefinition2); await manager.start(); - const action: SerializedAction = { + const action: SerializedAction = { factoryId: actionFactoryDefinition2.id, name: 'foo', config: {}, diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index 6ca388281ad76..471b929fdbc06 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -74,7 +74,7 @@ export class DynamicActionManager { const actionId = this.generateActionId(eventId); const factory = uiActions.getActionFactory(event.action.factoryId); - const actionDefinition: ActionDefinition = factory.create(action as SerializedAction); + const actionDefinition: ActionDefinition = factory.create(action as SerializedAction); uiActions.registerAction({ ...actionDefinition, id: actionId, @@ -195,10 +195,7 @@ export class DynamicActionManager { * @param action Dynamic action for which to create an event. * @param triggers List of triggers to which action should react. */ - public async createEvent( - action: SerializedAction, - triggers: Array - ) { + public async createEvent(action: SerializedAction, triggers: Array) { const event: SerializedEvent = { eventId: uuidv4(), triggers, @@ -231,7 +228,7 @@ export class DynamicActionManager { */ public async updateEvent( eventId: string, - action: SerializedAction, + action: SerializedAction, triggers: Array ) { const event: SerializedEvent = { diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts index d00db0d9acb7a..28d104093f64f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts @@ -5,21 +5,9 @@ */ import { TriggerId } from '../../../../../src/plugins/ui_actions/public'; +import { SerializedAction, SerializedEvent, BaseActionConfig } from '../../common/types'; -export interface SerializedAction { - readonly factoryId: string; - readonly name: string; - readonly config: Config; -} - -/** - * Serialized representation of a triggers-action pair, used to persist in storage. - */ -export interface SerializedEvent { - eventId: string; - triggers: string[]; - action: SerializedAction; -} +export { SerializedAction, SerializedEvent, BaseActionConfig }; /** * Action factory context passed into ActionFactories' CollectConfig, getDisplayName, getIconType diff --git a/x-pack/plugins/ui_actions_enhanced/public/index.ts b/x-pack/plugins/ui_actions_enhanced/public/index.ts index 4a899b24852a9..ae720598ec759 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/index.ts @@ -29,7 +29,10 @@ export { DynamicActionManagerState as UiActionsEnhancedDynamicActionManagerState, MemoryActionStorage as UiActionsEnhancedMemoryActionStorage, BaseActionFactoryContext as UiActionsEnhancedBaseActionFactoryContext, + BaseActionConfig as UiActionsEnhancedBaseActionConfig, } from './dynamic_actions'; +export { DynamicActionsState } from './services/ui_actions_service_enhancements'; + export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns'; export * from './drilldowns/url_drilldown'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts index 17a6fc1b955df..9eb0a06b6dbaf 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts @@ -30,6 +30,9 @@ const createStartContract = (): Start => { getActionFactories: jest.fn(), getActionFactory: jest.fn(), FlyoutManageDrilldowns: jest.fn(), + telemetry: jest.fn(), + extract: jest.fn(), + inject: jest.fn(), }; return startContract; diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index b38bc44abe2b0..b05c08c4c77d0 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -39,6 +39,7 @@ import { UiActionsServiceEnhancements } from './services'; import { ILicense, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { createFlyoutManageDrilldowns } from './drilldowns'; import { createStartServicesGetter, Storage } from '../../../../src/plugins/kibana_utils/public'; +import { dynamicActionEnhancement } from './dynamic_actions/dynamic_action_enhancement'; interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. @@ -58,7 +59,10 @@ export interface SetupContract export interface StartContract extends UiActionsStart, - Pick { + Pick< + UiActionsServiceEnhancements, + 'getActionFactory' | 'getActionFactories' | 'telemetry' | 'extract' | 'inject' + > { FlyoutManageDrilldowns: ReturnType; } @@ -87,7 +91,7 @@ export class AdvancedUiActionsPublicPlugin public setup( core: CoreSetup, - { uiActions, licensing }: SetupDependencies + { embeddable, uiActions, licensing }: SetupDependencies ): SetupContract { const startServices = createStartServicesGetter(core.getStartServices); this.enhancements = new UiActionsServiceEnhancements({ @@ -95,6 +99,7 @@ export class AdvancedUiActionsPublicPlugin featureUsageSetup: licensing.featureUsage, getFeatureUsageStart: () => startServices().plugins.licensing.featureUsage, }); + embeddable.registerEnhancement(dynamicActionEnhancement(this.enhancements)); return { ...uiActions, ...this.enhancements, diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts index 3a0b65d2ed844..6c71868222b24 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts @@ -96,6 +96,66 @@ describe('UiActionsService', () => { ).resolves.toBe(false); }); + test('action factory extract function gets called when calling uiactions extract', () => { + const service = new UiActionsServiceEnhancements(deps); + const actionState = { + events: [ + { + eventId: 'test', + triggers: [], + action: { factoryId: factoryDefinition1.id, name: 'test', config: {} }, + }, + ], + }; + const extract = jest.fn().mockImplementation((state) => ({ state, references: [] })); + service.registerActionFactory({ + ...factoryDefinition1, + extract, + }); + service.extract(actionState); + expect(extract).toBeCalledWith(actionState.events[0]); + }); + + test('action factory inject function gets called when calling uiactions inject', () => { + const service = new UiActionsServiceEnhancements(deps); + const actionState = { + events: [ + { + eventId: 'test', + triggers: [], + action: { factoryId: factoryDefinition1.id, name: 'test', config: {} }, + }, + ], + }; + const inject = jest.fn().mockImplementation((state) => state); + service.registerActionFactory({ + ...factoryDefinition1, + inject, + }); + service.inject(actionState, []); + expect(inject).toBeCalledWith(actionState.events[0], []); + }); + + test('action factory telemetry function gets called when calling uiactions telemetry', () => { + const service = new UiActionsServiceEnhancements(deps); + const actionState = { + events: [ + { + eventId: 'test', + triggers: [], + action: { factoryId: factoryDefinition1.id, name: 'test', config: {} }, + }, + ], + }; + const telemetry = jest.fn().mockImplementation((state) => ({})); + service.registerActionFactory({ + ...factoryDefinition1, + telemetry, + }); + service.telemetry(actionState); + expect(telemetry).toBeCalledWith(actionState.events[0], {}); + }); + describe('registerFeature for licensing', () => { const spy = jest.spyOn(deps.featureUsageSetup, 'register'); beforeEach(() => { diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index b8086c16f5e71..5e40d803962de 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -8,12 +8,20 @@ import { ActionFactoryRegistry } from '../types'; import { ActionFactory, ActionFactoryDefinition, + BaseActionConfig, BaseActionFactoryContext, + SerializedEvent, } from '../dynamic_actions'; import { DrilldownDefinition } from '../drilldowns'; import { ILicense } from '../../../licensing/common/types'; import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../../licensing/public'; +import { SavedObjectReference } from '../../../../../src/core/types'; +import { PersistableStateDefinition } from '../../../../../src/plugins/kibana_utils/common'; + +import { DynamicActionsState } from '../../common/types'; + +export { DynamicActionsState }; export interface UiActionsServiceEnhancementsParams { readonly actionFactories?: ActionFactoryRegistry; @@ -22,7 +30,8 @@ export interface UiActionsServiceEnhancementsParams { readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage']; } -export class UiActionsServiceEnhancements { +export class UiActionsServiceEnhancements + implements PersistableStateDefinition { protected readonly actionFactories: ActionFactoryRegistry; protected readonly deps: Omit; @@ -36,7 +45,7 @@ export class UiActionsServiceEnhancements { * serialize/deserialize dynamic actions. */ public readonly registerActionFactory = < - Config extends object = object, + Config extends BaseActionConfig = BaseActionConfig, SupportedTriggers extends TriggerId = TriggerId, FactoryContext extends BaseActionFactoryContext = { triggers: SupportedTriggers[]; @@ -81,7 +90,7 @@ export class UiActionsServiceEnhancements { * Convenience method to register a {@link DrilldownDefinition | drilldown}. */ public readonly registerDrilldown = < - Config extends object = object, + Config extends BaseActionConfig = BaseActionConfig, SupportedTriggers extends TriggerId = TriggerId, FactoryContext extends BaseActionFactoryContext = { triggers: SupportedTriggers[]; @@ -89,6 +98,7 @@ export class UiActionsServiceEnhancements { ExecutionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] >({ id: factoryId, + isBeta, order, CollectConfig, createConfig, @@ -101,6 +111,9 @@ export class UiActionsServiceEnhancements { licenseFeatureName, supportedTriggers, isCompatible, + telemetry, + extract, + inject, }: DrilldownDefinition): void => { const actionFactory: ActionFactoryDefinition< Config, @@ -109,6 +122,7 @@ export class UiActionsServiceEnhancements { ExecutionContext > = { id: factoryId, + isBeta, minimalLicense, licenseFeatureName, order, @@ -117,6 +131,9 @@ export class UiActionsServiceEnhancements { isConfigValid, getDisplayName, supportedTriggers, + telemetry, + extract, + inject, getIconType: () => euiIcon, isCompatible: async () => true, create: (serializedAction) => ({ @@ -149,4 +166,43 @@ export class UiActionsServiceEnhancements { ); }); }; + + public readonly telemetry = (state: DynamicActionsState, telemetry: Record = {}) => { + let telemetryData = telemetry; + state.events.forEach((event: SerializedEvent) => { + if (this.actionFactories.has(event.action.factoryId)) { + telemetryData = this.actionFactories + .get(event.action.factoryId)! + .telemetry(event, telemetryData); + } + }); + return telemetryData; + }; + + public readonly extract = (state: DynamicActionsState) => { + const references: SavedObjectReference[] = []; + const newState = { + events: state.events.map((event: SerializedEvent) => { + const result = this.actionFactories.has(event.action.factoryId) + ? this.actionFactories.get(event.action.factoryId)!.extract(event) + : { + state: event, + references: [], + }; + references.push(...result.references); + return result.state; + }), + }; + return { state: newState, references }; + }; + + public readonly inject = (state: DynamicActionsState, references: SavedObjectReference[]) => { + return { + events: state.events.map((event: SerializedEvent) => { + return this.actionFactories.has(event.action.factoryId) + ? this.actionFactories.get(event.action.factoryId)!.inject(event, references) + : event; + }), + }; + }; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts b/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts index 3d143b0cacd06..9a529f192158d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts @@ -17,7 +17,6 @@ import { TimeRange } from '../../../../../src/plugins/data/public'; * https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type * here instead */ -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type InheritedChildrenInput = { timeRange: TimeRange; id?: string; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts new file mode 100644 index 0000000000000..b366436200914 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; +import { SavedObjectReference } from '../../../../src/core/types'; +import { DynamicActionsState, SerializedEvent } from './types'; +import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; + +export const dynamicActionEnhancement = ( + uiActionsEnhanced: AdvancedUiActionsPublicPlugin +): EnhancementRegistryDefinition => { + return { + id: 'dynamicActions', + telemetry: (state: SerializableState, telemetry: Record) => { + let telemetryData = telemetry; + (state as DynamicActionsState).events.forEach((event: SerializedEvent) => { + if (uiActionsEnhanced.getActionFactory(event.action.factoryId)) { + telemetryData = uiActionsEnhanced + .getActionFactory(event.action.factoryId)! + .telemetry(event, telemetryData); + } + }); + return telemetryData; + }, + extract: (state: SerializableState) => { + const references: SavedObjectReference[] = []; + const newState: DynamicActionsState = { + events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { + const result = uiActionsEnhanced.getActionFactory(event.action.factoryId) + ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.extract(event) + : { + state: event, + references: [], + }; + result.references.forEach((r) => references.push(r)); + return result.state; + }), + }; + return { state: newState, references }; + }, + inject: (state: SerializableState, references: SavedObjectReference[]) => { + return { + events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { + return uiActionsEnhanced.getActionFactory(event.action.factoryId) + ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.inject(event, references) + : event; + }), + } as DynamicActionsState; + }, + } as EnhancementRegistryDefinition; +}; diff --git a/x-pack/plugins/ui_actions_enhanced/server/index.ts b/x-pack/plugins/ui_actions_enhanced/server/index.ts new file mode 100644 index 0000000000000..5419c4135796d --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AdvancedUiActionsPublicPlugin } from './plugin'; + +export function plugin() { + return new AdvancedUiActionsPublicPlugin(); +} + +export { AdvancedUiActionsPublicPlugin as Plugin }; +export { + SetupContract as AdvancedUiActionsSetup, + StartContract as AdvancedUiActionsStart, +} from './plugin'; + +export { + ActionFactoryDefinition as UiActionsEnhancedActionFactoryDefinition, + ActionFactory as UiActionsEnhancedActionFactory, +} from './types'; + +export { + DynamicActionsState, + BaseActionConfig as UiActionsEnhancedBaseActionConfig, + SerializedAction as UiActionsEnhancedSerializedAction, + SerializedEvent as UiActionsEnhancedSerializedEvent, +} from '../common/types'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts new file mode 100644 index 0000000000000..0a61c917a2c5c --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { identity } from 'lodash'; +import { CoreSetup, Plugin, SavedObjectReference } from '../../../../src/core/server'; +import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server'; +import { dynamicActionEnhancement } from './dynamic_action_enhancement'; +import { + ActionFactoryRegistry, + SerializedEvent, + ActionFactoryDefinition, + DynamicActionsState, +} from './types'; + +export interface SetupContract { + registerActionFactory: any; +} + +export type StartContract = void; + +interface SetupDependencies { + embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. +} + +export class AdvancedUiActionsPublicPlugin + implements Plugin { + protected readonly actionFactories: ActionFactoryRegistry = new Map(); + + constructor() {} + + public setup(core: CoreSetup, { embeddable }: SetupDependencies) { + embeddable.registerEnhancement(dynamicActionEnhancement(this)); + + return { + registerActionFactory: this.registerActionFactory, + }; + } + + public start() {} + + public stop() {} + + /** + * Register an action factory. Action factories are used to configure and + * serialize/deserialize dynamic actions. + */ + public readonly registerActionFactory = (definition: ActionFactoryDefinition) => { + if (this.actionFactories.has(definition.id)) { + throw new Error(`ActionFactory [actionFactory.id = ${definition.id}] already registered.`); + } + + this.actionFactories.set(definition.id, { + id: definition.id, + telemetry: definition.telemetry || (() => ({})), + inject: definition.inject || identity, + extract: + definition.extract || + ((state: SerializedEvent) => { + return { state, references: [] }; + }), + }); + }; + + public readonly getActionFactory = (actionFactoryId: string) => { + const actionFactory = this.actionFactories.get(actionFactoryId); + return actionFactory; + }; + + public readonly telemetry = (state: DynamicActionsState, telemetry: Record = {}) => { + state.events.forEach((event: SerializedEvent) => { + if (this.actionFactories.has(event.action.factoryId)) { + this.actionFactories.get(event.action.factoryId)!.telemetry(event, telemetry); + } + }); + return telemetry; + }; + + public readonly extract = (state: DynamicActionsState) => { + const references: SavedObjectReference[] = []; + const newState = { + events: state.events.map((event: SerializedEvent) => { + const result = this.actionFactories.has(event.action.factoryId) + ? this.actionFactories.get(event.action.factoryId)!.extract(event) + : { + state: event, + references: [], + }; + result.references.forEach((r) => references.push(r)); + return result.state; + }), + }; + return { state: newState, references }; + }; + + public readonly inject = (state: DynamicActionsState, references: SavedObjectReference[]) => { + return { + events: state.events.map((event: SerializedEvent) => { + return this.actionFactories.has(event.action.factoryId) + ? this.actionFactories.get(event.action.factoryId)!.inject(event, references) + : event; + }), + }; + }; +} diff --git a/x-pack/plugins/ui_actions_enhanced/server/types.ts b/x-pack/plugins/ui_actions_enhanced/server/types.ts new file mode 100644 index 0000000000000..4859be6728344 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/server/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + PersistableState, + PersistableStateDefinition, +} from '../../../../src/plugins/kibana_utils/common'; + +import { SerializedAction, SerializedEvent, DynamicActionsState } from '../common/types'; + +export type ActionFactoryRegistry = Map; + +export interface ActionFactoryDefinition

+ extends PersistableStateDefinition

{ + id: string; +} + +export interface ActionFactory

+ extends PersistableState

{ + id: string; +} + +export { SerializedEvent, SerializedAction, DynamicActionsState }; diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts index a29c37c9a988c..2a94b39ca6c66 100644 --- a/x-pack/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/plugins/upgrade_assistant/common/types.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; import { SavedObject, SavedObjectAttributes } from 'src/core/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../src/core/server/elasticsearch/legacy/api_types'; export enum ReindexStep { // Enum values are spaced out by 10 to give us room to insert steps in between. diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx index b7eafb7bf5c88..66c802097055b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/constants.tsx @@ -6,8 +6,8 @@ import { IconColor } from '@elastic/eui'; import { invert } from 'lodash'; - -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; export const LEVEL_MAP: { [level: string]: number } = { warning: 0, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx index 60db71c76357a..d75a25a95d67f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/controls.tsx @@ -8,8 +8,8 @@ import React, { FunctionComponent, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { GroupByOption, LevelFilterOption, LoadingState } from '../../types'; import { FilterBar } from './filter_bar'; import { GroupByBar } from './group_by_bar'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx index f2469fe1225e3..affeeb84f35d7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.test.tsx @@ -7,10 +7,10 @@ import { range } from 'lodash'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; - import { EuiBadge, EuiPagination } from '@elastic/eui'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; import { DeprecationAccordion, filterDeps, GroupedDeprecations } from './grouped'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx index bdc7fa5e58e8a..de1a5a996d75f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/grouped.tsx @@ -18,7 +18,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption, LevelFilterOption } from '../../../types'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx index 72a36d72bab6e..3ce40d0c4fdf0 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/health.tsx @@ -10,7 +10,8 @@ import React, { FunctionComponent } from 'react'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { COLOR_MAP, LEVEL_MAP, REVERSE_LEVEL_MAP } from '../constants'; const LocalizedLevels: { [level: string]: string } = { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx index 5e11e4d0a5283..038f05aace4c3 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/list.tsx @@ -6,7 +6,8 @@ import React, { FunctionComponent } from 'react'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; import { GroupByOption } from '../../../types'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx index 9d0c9ea9ddb56..053ef21d6b309 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.test.tsx @@ -7,7 +7,8 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { LevelFilterOption } from '../../types'; import { FilterBar } from './filter_bar'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx index 0dafe2105cdb7..6939c547fee57 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/filter_bar.tsx @@ -10,7 +10,8 @@ import React from 'react'; import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationInfo } from '../../../../../../../../src/core/server/elasticsearch/legacy/api_types'; import { LevelFilterOption } from '../../types'; const LocalizedOptions: { [option: string]: string } = { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts index 6e524a98afdc6..f97a056d5cd36 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts @@ -6,7 +6,8 @@ import _ from 'lodash'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -import { DeprecationAPIResponse } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationAPIResponse } from '../../../../../src/core/server/elasticsearch/legacy/api_types'; import { getUpgradeAssistantStatus } from './es_migration_apis'; import fakeDeprecations from './__fixtures__/fake_deprecations.json'; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts index abbeb8a89e12a..9f55b9d049735 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.ts @@ -5,7 +5,8 @@ */ import { ILegacyScopedClusterClient } from 'src/core/server'; -import { DeprecationAPIResponse } from 'src/legacy/core_plugins/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationAPIResponse } from '../../../../../src/core/server/elasticsearch/legacy/api_types'; import { EnrichedDeprecationInfo, UpgradeAssistantStatus } from '../../common/types'; import { esIndicesStateCheck } from './es_indices_state_check'; diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 5c3211eff3b4e..cd2dc5018e110 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -5,6 +5,7 @@ */ import { Request, Server } from 'hapi'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { PLUGIN } from '../common/constants/plugin'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; @@ -31,6 +32,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor id: PLUGIN.ID, name: PLUGIN.NAME, order: 1000, + category: DEFAULT_APP_CATEGORIES.observability, navLinkId: PLUGIN.ID, icon: 'uptimeApp', app: ['uptime', 'kibana'], diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index e11de1376e400..f4553e4c3a6fe 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -51,7 +51,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('a11y test for for customize space card', async () => { await PageObjects.spaceSelector.clickEnterSpaceName(); await PageObjects.spaceSelector.addSpaceName('space_a'); - await PageObjects.spaceSelector.clickSpaceAcustomAvatar(); + await PageObjects.spaceSelector.clickCustomizeSpaceAvatar('space_a'); await a11y.testAppSnapshot(); await browser.pressKeys(browser.keys.ESCAPE); }); @@ -75,30 +75,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - it('a11y test for click on "show" button to open customize feature display', async () => { - await retry.waitFor( - 'show button is visible', - async () => await testSubjects.exists('show-hide-section-link') - ); - await PageObjects.spaceSelector.clickShowFeatures(); - await a11y.testAppSnapshot(); - }); - - it('a11y test for change all option for feature visibility popover', async () => { - await PageObjects.spaceSelector.clickFeaturesVisibilityButton(); + it('a11y test for toggling an entire feature category', async () => { + await PageObjects.spaceSelector.toggleFeatureCategoryVisibility('kibana'); await a11y.testAppSnapshot(); - }); - it('a11y test for hide all feature visibility popover option', async () => { - await PageObjects.spaceSelector.clickHideAllFeatures(); + await PageObjects.spaceSelector.openFeatureCategory('kibana'); await a11y.testAppSnapshot(); - }); - it('a11y test for toggle individual feature - using enterprise feature visibility', async () => { - await PageObjects.spaceSelector.clickFeaturesVisibilityButton(); - await PageObjects.spaceSelector.clickShowAllFeatures(); - await PageObjects.spaceSelector.toggleFeatureVisibility('enterpriseSearch'); - await a11y.testAppSnapshot(); + await PageObjects.spaceSelector.toggleFeatureCategoryVisibility('kibana'); }); it('a11y test for space listing page', async () => { 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 68ff3dad9ae86..43e4f642bb943 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 @@ -76,6 +76,7 @@ export class FixturePlugin implements Plugin(type, id); + const result = await savedObjectsWithAlerts.update( + type, + id, + { ...savedAlert.attributes, ...attributes }, + options + ); return res.ok({ body: result }); } ); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts index e1ef1255c6e13..f3c31716fbcda 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts @@ -27,6 +27,7 @@ export class FixturePlugin implements Plugin { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'test') + .send({ + name: 'A generic Webhook action', + actionTypeId: '.webhook', + secrets: { + user: 'username', + password: 'mypassphrase', + }, + config: { + url: webhookSimulatorURL, + headers: { + someHeader: '123', + }, + }, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + isPreconfigured: false, + name: 'A generic Webhook action', + actionTypeId: '.webhook', + config: { + ...defaultValues, + url: webhookSimulatorURL, + headers: { + someHeader: '123', + }, + }, + }); + + await supertest + .put(`/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'A generic Webhook action', + secrets: { + user: 'username', + password: 'mypassphrase', + }, + config: { + url: webhookSimulatorURL, + headers: { + someOtherHeader: '456', + }, + }, + }) + .expect(200); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/action/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, + name: 'A generic Webhook action', + actionTypeId: '.webhook', + config: { + ...defaultValues, + url: webhookSimulatorURL, + headers: { + someOtherHeader: '456', + }, + }, + }); + }); + it('should send authentication to the webhook target', async () => { const webhookActionId = await createWebhookAction(webhookSimulatorURL, {}, kibanaURL); const { body: result } = await supertest diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js index bc8b2af401423..bb35f6fd96429 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js @@ -29,7 +29,7 @@ export default function ({ getService }) { const nodesIds = Object.keys(nodeStats.nodes); const { body } = await loadNodes().expect(200); - expect(body[NODE_CUSTOM_ATTRIBUTE]).to.eql(nodesIds); + expect(body.nodesByAttributes[NODE_CUSTOM_ATTRIBUTE]).to.eql(nodesIds); }); }); diff --git a/x-pack/test/api_integration/config_security_basic.ts b/x-pack/test/api_integration/config_security_basic.ts index 8489940505686..237d162e80328 100644 --- a/x-pack/test/api_integration/config_security_basic.ts +++ b/x-pack/test/api_integration/config_security_basic.ts @@ -19,6 +19,7 @@ export default async function (context: FtrConfigProviderContext) { 'xpack.security.authc.api_key.enabled=true', ]; config.testFiles = [require.resolve('./apis/security/security_basic')]; + config.junit.reportName = 'X-Pack API Integration Tests (Security Basic)'; return config; }); } diff --git a/x-pack/test/api_integration/config_security_trial.ts b/x-pack/test/api_integration/config_security_trial.ts index 4c1e2913b987c..839165d8618cf 100644 --- a/x-pack/test/api_integration/config_security_trial.ts +++ b/x-pack/test/api_integration/config_security_trial.ts @@ -12,6 +12,7 @@ import { default as createTestConfig } from './config'; export default async function (context: FtrConfigProviderContext) { return createTestConfig(context).then((config) => { config.testFiles = [require.resolve('./apis/security/security_trial')]; + config.junit.reportName = 'X-Pack API Integration Tests (Security Trial)'; return config; }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts index 400d0d294bf02..e0e13b7b7fb98 100644 --- a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts @@ -149,13 +149,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext) log.error(JSON.stringify(res, null, 2)); }, }, - { - req: { - url: `/api/apm/settings/custom_links`, - }, - expectForbidden: expect404, - expectResponse: expect200, - }, { req: { url: `/api/apm/settings/custom_links/transaction`, diff --git a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts index f82e16e090eae..ad3d1b0ccc4d9 100644 --- a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts @@ -19,7 +19,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('when data is loaded', () => { + // FLAKY: https://github.com/elastic/kibana/issues/77870 + describe.skip('when data is loaded', () => { before(() => esArchiver.load('metrics_8.0.0')); after(() => esArchiver.unload('metrics_8.0.0')); diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts index a1c647a854bf6..60b4020e73dce 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts @@ -3,75 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import URL from 'url'; import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { CustomLink } from '../../../../../plugins/apm/common/custom_link/custom_link_types'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function customLinksTests({ getService }: FtrProviderContext) { - const supertestRead = getService('supertestAsApmReadUser'); const supertestWrite = getService('supertestAsApmWriteUser'); - const log = getService('log'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - - function searchCustomLinks(filters?: any) { - const path = URL.format({ - pathname: `/api/apm/settings/custom_links`, - query: filters, - }); - return supertestRead.get(path).set('kbn-xsrf', 'foo'); - } - - async function createCustomLink(customLink: CustomLink) { - log.debug('creating configuration', customLink); - const res = await supertestWrite - .post(`/api/apm/settings/custom_links`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - async function updateCustomLink(id: string, customLink: CustomLink) { - log.debug('updating configuration', id, customLink); - const res = await supertestWrite - .put(`/api/apm/settings/custom_links/${id}`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - async function deleteCustomLink(id: string) { - log.debug('deleting configuration', id); - const res = await supertestWrite - .delete(`/api/apm/settings/custom_links/${id}`) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - function throwOnError(res: any) { - const { statusCode, req, body } = res; - if (statusCode !== 200) { - throw new Error(` - Endpoint: ${req.method} ${req.path} - Service: ${JSON.stringify(res.request._data.service)} - Status code: ${statusCode} - Response: ${body.message}`); - } - } describe('custom links', () => { - before(async () => { + it('is only be available to users with Gold license (or higher)', async () => { const customLink = { url: 'https://elastic.co', label: 'with filters', @@ -80,80 +21,16 @@ export default function customLinksTests({ getService }: FtrProviderContext) { { key: 'transaction.type', value: 'qux' }, ], } as CustomLink; - await createCustomLink(customLink); - }); - it('fetches a custom link', async () => { - const { status, body } = await searchCustomLinks({ - 'service.name': 'baz', - 'transaction.type': 'qux', - }); - const { label, url, filters } = body[0]; - - expect(status).to.equal(200); - expect({ label, url, filters }).to.eql({ - label: 'with filters', - url: 'https://elastic.co', - filters: [ - { key: 'service.name', value: 'baz' }, - { key: 'transaction.type', value: 'qux' }, - ], - }); - }); - it('updates a custom link', async () => { - let { status, body } = await searchCustomLinks({ - 'service.name': 'baz', - 'transaction.type': 'qux', - }); - expect(status).to.equal(200); - await updateCustomLink(body[0].id, { - label: 'foo', - url: 'https://elastic.co?service.name={{service.name}}', - filters: [ - { key: 'service.name', value: 'quz' }, - { key: 'transaction.name', value: 'bar' }, - ], - }); - ({ status, body } = await searchCustomLinks({ - 'service.name': 'quz', - 'transaction.name': 'bar', - })); - const { label, url, filters } = body[0]; - expect(status).to.equal(200); - expect({ label, url, filters }).to.eql({ - label: 'foo', - url: 'https://elastic.co?service.name={{service.name}}', - filters: [ - { key: 'service.name', value: 'quz' }, - { key: 'transaction.name', value: 'bar' }, - ], - }); - }); - it('deletes a custom link', async () => { - let { status, body } = await searchCustomLinks({ - 'service.name': 'quz', - 'transaction.name': 'bar', - }); - expect(status).to.equal(200); - await deleteCustomLink(body[0].id); - ({ status, body } = await searchCustomLinks({ - 'service.name': 'quz', - 'transaction.name': 'bar', - })); - expect(status).to.equal(200); - expect(body).to.eql([]); - }); + const response = await supertestWrite + .post(`/api/apm/settings/custom_links`) + .send(customLink) + .set('kbn-xsrf', 'foo'); - describe('transaction', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); + expect(response.status).to.be(403); - it('fetches a transaction sample', async () => { - const response = await supertestRead.get( - '/api/apm/settings/custom_links/transaction?service.name=opbeans-java' - ); - expect(response.status).to.be(200); - expect(response.body.service.name).to.eql('opbeans-java'); - }); + expectSnapshot(response.body.message).toMatchInline( + `"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services."` + ); }); }); } diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts index 18beb76e5a3a0..6364a79a12f04 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts @@ -71,7 +71,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) 0, 0, ], - "tbt": "0.00", + "tbt": 0, } `); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index bf32c4661afd5..ae62253c62d81 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -19,6 +19,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr }); describe('Settings', function () { + loadTestFile(require.resolve('./settings/custom_link.ts')); describe('Anomaly detection', function () { loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); loadTestFile(require.resolve('./settings/anomaly_detection/read_user')); diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap index 1424ca42539c0..199a49dce8f9e 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap @@ -77,6 +77,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -104,6 +105,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -130,6 +132,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -156,6 +159,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -209,6 +213,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -255,6 +260,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -301,6 +307,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -407,6 +414,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -465,6 +473,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -491,6 +500,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -504,6 +514,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -523,6 +534,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -548,6 +560,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -573,6 +586,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -605,6 +619,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -687,6 +702,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -734,6 +750,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -748,6 +765,7 @@ Array [ "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -806,6 +824,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -833,6 +852,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -859,6 +879,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -885,6 +906,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -938,6 +960,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -984,6 +1007,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1030,6 +1054,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1136,6 +1161,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1194,6 +1220,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1220,6 +1247,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1233,6 +1261,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1252,6 +1281,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1277,6 +1307,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1302,6 +1333,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1334,6 +1366,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1416,6 +1449,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, @@ -1463,6 +1497,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -1477,6 +1512,7 @@ Object { "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index be3301964bd3c..11b5ca71e64e7 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -165,6 +165,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "serviceAnomalyStats": Object { "actualValue": 3933482.1764705875, "anomalyScore": 2.6101702751482714, + "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", }, @@ -179,6 +180,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "serviceAnomalyStats": Object { "actualValue": 684716.5813953485, "anomalyScore": 0.20498907719907372, + "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", }, diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index d08064f6aa70e..c93816dfb48b9 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -45,24 +45,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.body.items.length).to.be.greaterThan(0); }); - it('some items have severity set', () => { + it('some items have a health status set', () => { // Under the assumption that the loaded archive has // at least one APM ML job, and the time range is longer - // than 15m, at least one items should have severity set. - // Note that we currently have a bug where healthy services - // report as unknown (so without any severity status): + // than 15m, at least one items should have a health status + // set. Note that we currently have a bug where healthy + // services report as unknown (so without any health status): // https://github.com/elastic/kibana/issues/77083 - const severityScores = response.body.items.map((item: any) => item.severity); + const healthStatuses = response.body.items.map((item: any) => item.healthStatus); - expect(severityScores.filter(Boolean).length).to.be.greaterThan(0); + expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0); - expectSnapshot(severityScores).toMatchInline(` + expectSnapshot(healthStatuses).toMatchInline(` Array [ undefined, undefined, - "warning", - "warning", + "healthy", + "healthy", undefined, undefined, undefined, diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts new file mode 100644 index 0000000000000..bcfe8fce4b948 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/settings/custom_link.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import URL from 'url'; +import expect from '@kbn/expect'; +import { CustomLink } from '../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function customLinksTests({ getService }: FtrProviderContext) { + const supertestRead = getService('supertest'); + const supertestWrite = getService('supertestAsApmWriteUser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + + function searchCustomLinks(filters?: any) { + const path = URL.format({ + pathname: `/api/apm/settings/custom_links`, + query: filters, + }); + return supertestRead.get(path).set('kbn-xsrf', 'foo'); + } + + async function createCustomLink(customLink: CustomLink) { + log.debug('creating configuration', customLink); + const res = await supertestWrite + .post(`/api/apm/settings/custom_links`) + .send(customLink) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function updateCustomLink(id: string, customLink: CustomLink) { + log.debug('updating configuration', id, customLink); + const res = await supertestWrite + .put(`/api/apm/settings/custom_links/${id}`) + .send(customLink) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function deleteCustomLink(id: string) { + log.debug('deleting configuration', id); + const res = await supertestWrite + .delete(`/api/apm/settings/custom_links/${id}`) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + function throwOnError(res: any) { + const { statusCode, req, body } = res; + if (statusCode !== 200) { + throw new Error(` + Endpoint: ${req.method} ${req.path} + Service: ${JSON.stringify(res.request._data.service)} + Status code: ${statusCode} + Response: ${body.message}`); + } + } + + describe('custom links', () => { + before(async () => { + const customLink = { + url: 'https://elastic.co', + label: 'with filters', + filters: [ + { key: 'service.name', value: 'baz' }, + { key: 'transaction.type', value: 'qux' }, + ], + } as CustomLink; + await createCustomLink(customLink); + }); + it('fetches a custom link', async () => { + const { status, body } = await searchCustomLinks({ + 'service.name': 'baz', + 'transaction.type': 'qux', + }); + const { label, url, filters } = body[0]; + + expect(status).to.equal(200); + expect({ label, url, filters }).to.eql({ + label: 'with filters', + url: 'https://elastic.co', + filters: [ + { key: 'service.name', value: 'baz' }, + { key: 'transaction.type', value: 'qux' }, + ], + }); + }); + it('updates a custom link', async () => { + let { status, body } = await searchCustomLinks({ + 'service.name': 'baz', + 'transaction.type': 'qux', + }); + expect(status).to.equal(200); + await updateCustomLink(body[0].id, { + label: 'foo', + url: 'https://elastic.co?service.name={{service.name}}', + filters: [ + { key: 'service.name', value: 'quz' }, + { key: 'transaction.name', value: 'bar' }, + ], + }); + ({ status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + })); + const { label, url, filters } = body[0]; + expect(status).to.equal(200); + expect({ label, url, filters }).to.eql({ + label: 'foo', + url: 'https://elastic.co?service.name={{service.name}}', + filters: [ + { key: 'service.name', value: 'quz' }, + { key: 'transaction.name', value: 'bar' }, + ], + }); + }); + it('deletes a custom link', async () => { + let { status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + }); + expect(status).to.equal(200); + await deleteCustomLink(body[0].id); + ({ status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + })); + expect(status).to.equal(200); + expect(body).to.eql([]); + }); + + describe('transaction', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('fetches a transaction sample', async () => { + const response = await supertestRead.get( + '/api/apm/settings/custom_links/transaction?service.name=opbeans-java' + ); + expect(response.status).to.be(200); + expect(response.body.service.name).to.eql('opbeans-java'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js index bd35374643e9b..b74df71701026 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js @@ -40,18 +40,21 @@ export default function ({ getPageObjects, getService }) { operation: 'date_histogram', field: '@timestamp', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'avg', field: 'bytes', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', operation: 'terms', field: 'ip', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.save(title, saveAsNew, redirectToOrigin); } diff --git a/x-pack/test/functional/apps/home/feature_controls/home_security.ts b/x-pack/test/functional/apps/home/feature_controls/home_security.ts new file mode 100644 index 0000000000000..6074bba372cb2 --- /dev/null +++ b/x-pack/test/functional/apps/home/feature_controls/home_security.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['security', 'home']); + const testSubjects = getService('testSubjects'); + + describe('security', () => { + before(async () => { + await esArchiver.load('dashboard/feature_controls/security'); + await esArchiver.loadIfNeeded('logstash_functional'); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + await esArchiver.unload('dashboard/feature_controls/security'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global all privileges', () => { + before(async () => { + await security.role.create('global_all_role', { + elasticsearch: {}, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_all_user', { + password: 'global_all_user-password', + roles: ['global_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login('global_all_user', 'global_all_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await security.role.delete('global_all_role'); + await security.user.delete('global_all_user'); + }); + + it('shows all available solutions', async () => { + const solutions = await PageObjects.home.getVisibileSolutions(); + expect(solutions).to.eql([ + 'enterpriseSearch', + 'observability', + 'securitySolution', + 'kibana', + ]); + }); + + it('shows the management section', async () => { + await testSubjects.existOrFail('homDataManage', { timeout: 2000 }); + }); + + it('shows the "Manage" action item', async () => { + await testSubjects.existOrFail('homManagementActionItem', { + timeout: 2000, + }); + }); + }); + + describe('global dashboard all privileges', () => { + before(async () => { + await security.role.create('global_dashboard_all_role', { + elasticsearch: {}, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dashboard_all_user', { + password: 'global_dashboard_all_user-password', + roles: ['global_dashboard_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dashboard_all_user', + 'global_dashboard_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dashboard_all_role'); + await security.user.delete('global_dashboard_all_user'); + }); + + it('shows only the kibana solution', async () => { + const solutions = await PageObjects.home.getVisibileSolutions(); + expect(solutions).to.eql(['kibana']); + }); + + it('does not show the management section', async () => { + await testSubjects.missingOrFail('homDataManage', { timeout: 2000 }); + }); + + it('does not show the "Manage" action item', async () => { + await testSubjects.missingOrFail('homManagementActionItem', { + timeout: 2000, + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/home/feature_controls/index.ts b/x-pack/test/functional/apps/home/feature_controls/index.ts new file mode 100644 index 0000000000000..70185d511f2c1 --- /dev/null +++ b/x-pack/test/functional/apps/home/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('feature controls', function () { + this.tags('skipFirefox'); + loadTestFile(require.resolve('./home_security')); + }); +}; diff --git a/x-pack/test/functional/apps/home/index.ts b/x-pack/test/functional/apps/home/index.ts new file mode 100644 index 0000000000000..1e14411ffc2b0 --- /dev/null +++ b/x-pack/test/functional/apps/home/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('Home page', function () { + this.tags('ciGroup7'); + loadTestFile(require.resolve('./feature_controls')); + }); +}; diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 4a68c9a8ff3f2..fa13d013ea115 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -51,7 +51,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await find.clickByButtonText('lnsXYvis'); await dashboardAddPanel.closeAddPanel(); await PageObjects.lens.goToTimeRange(); - await clickInChart(5, 5); // hardcoded position of bar + await clickInChart(5, 5); // hardcoded position of bar, depends heavy on data and charts implementation await retry.try(async () => { await testSubjects.click('applyFiltersPopoverButton'); @@ -68,5 +68,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248'); expect(hasIpFilter).to.be(true); }); + it('should be able to add filters by clicking in pie chart', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + await PageObjects.lens.goToTimeRange(); + await clickInChart(5, 5); // hardcoded position of the slice, depends heavy on data and charts implementation + + await PageObjects.lens.assertExactText( + '[data-test-subj="embeddablePanelHeading-lnsPieVis"]', + 'lnsPieVis' + ); + const hasGeoDestFilter = await filterBar.hasFilter('geo.dest', 'LS'); + expect(hasGeoDestFilter).to.be(true); + }); }); } diff --git a/x-pack/test/functional/apps/lens/lens_reporting.ts b/x-pack/test/functional/apps/lens/lens_reporting.ts index 4974b63be6f72..751fbbce13add 100644 --- a/x-pack/test/functional/apps/lens/lens_reporting.ts +++ b/x-pack/test/functional/apps/lens/lens_reporting.ts @@ -12,10 +12,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const esArchiver = getService('esArchiver'); const listingTable = getService('listingTable'); + const security = getService('security'); describe('lens reporting', () => { before(async () => { await esArchiver.loadIfNeeded('lens/reporting'); + await security.testUser.setRoles( + ['test_logstash_reader', 'global_dashboard_read', 'reporting_user'], + false + ); }); after(async () => { @@ -25,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { refresh: true, body: { query: { match_all: {} } }, }); + await security.testUser.restoreDefaults(); }); it('should not cause PDF reports to fail', async () => { @@ -33,7 +39,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.reporting.openPdfReportingPanel(); await PageObjects.reporting.clickGenerateReportButton(); const url = await PageObjects.reporting.getReportURL(60000); - expect(url).to.be.ok(); }); }); diff --git a/x-pack/test/functional/apps/lens/rollup.ts b/x-pack/test/functional/apps/lens/rollup.ts index f6882c8aed214..8e1dc231b6b1a 100644 --- a/x-pack/test/functional/apps/lens/rollup.ts +++ b/x-pack/test/functional/apps/lens/rollup.ts @@ -34,18 +34,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'date_histogram', field: '@timestamp', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'sum', field: 'bytes', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', operation: 'terms', field: 'geo.src', }); + await PageObjects.lens.closeDimensionEditor(); expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); await PageObjects.lens.save('Afancilenstest'); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 8c4321d77acf4..42807a23cb13a 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -8,20 +8,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['visualize', 'lens']); + const PageObjects = getPageObjects(['visualize', 'lens', 'common']); const find = getService('find'); const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); describe('lens smokescreen tests', () => { - it('should allow editing saved visualizations', async () => { - await PageObjects.visualize.gotoVisualizationLandingPage(); - await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); - await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); - await PageObjects.lens.goToTimeRange(); - await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); - }); - it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); @@ -32,18 +24,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'date_histogram', field: '@timestamp', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'avg', field: 'bytes', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension', operation: 'terms', field: '@message.raw', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.switchToVisualization('lnsDatatable'); await PageObjects.lens.removeDimension('lnsDatatable_column'); @@ -54,6 +49,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'terms', field: 'ip', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.save('Afancilenstest'); @@ -70,8 +66,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // legend item(s), so we're using a class selector here. expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3); }); + it('should create an xy visualization with filters aggregation', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', + operation: 'filters', + isPreviousIncompatible: true, + }); + await PageObjects.lens.addFilterToAgg(`geo.src : CN`); + + expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]); + expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2); + }); - it('should allow seamless transition to and from table view', async () => { + it('should transition from metric to table to metric', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); @@ -84,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); }); - it('should switch from a multi-layer stacked bar to a multi-layer line chart', async () => { + it('should transition from a multi-layer stacked bar to a multi-layer line chart and correctly remove all layers', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); @@ -95,22 +106,75 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: '@timestamp', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'avg', field: 'bytes', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.createLayer(); expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false); await PageObjects.lens.switchToVisualization('line'); + await PageObjects.lens.configureDimension( + { + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }, + 1 + ); + + await PageObjects.lens.closeDimensionEditor(); + await PageObjects.lens.configureDimension( + { + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }, + 1 + ); + await PageObjects.lens.closeDimensionEditor(); expect(await PageObjects.lens.getLayerCount()).to.eql(2); + await testSubjects.click('lnsLayerRemove'); + await testSubjects.click('lnsLayerRemove'); + await testSubjects.existOrFail('empty-workspace'); + }); + + it('should edit settings of xy line chart', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + await testSubjects.click('lnsXY_splitDimensionPanel > indexPattern-dimension-remove'); + await PageObjects.lens.switchToVisualization('line'); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'max', + field: 'memory', + }); + await PageObjects.lens.editDimensionLabel('Test of label'); + await PageObjects.lens.editDimensionFormat('Percent'); + await PageObjects.lens.editDimensionColor('#ff0000'); + await PageObjects.lens.editMissingValues('Linear'); + + await PageObjects.lens.assertMissingValues('Linear'); + await PageObjects.lens.assertColor('#ff0000'); + + await testSubjects.existOrFail('indexPattern-dimension-formatDecimals'); + + await PageObjects.lens.closeDimensionEditor(); + + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( + 'Test of label' + ); }); - it('should switch from a multi-layer stacked bar to donut chart using suggestions', async () => { + it('should transition from a multi-layer stacked bar to donut chart using suggestions', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); @@ -121,12 +185,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'geo.dest', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'avg', field: 'bytes', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.createLayer(); await PageObjects.lens.configureDimension( @@ -138,6 +204,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 1 ); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension( { dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', @@ -146,6 +213,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, 1 ); + + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.save('twolayerchart'); await testSubjects.click('lnsSuggestion-asDonut > lnsSuggestion'); @@ -158,7 +227,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should allow transition from line chart to donut chart and to bar chart', async () => { + it('should transition from line chart to donut chart and to bar chart', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); @@ -185,7 +254,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should allow seamless transition from bar chart to line chart using layer chart switch', async () => { + it('should transition from bar chart to line chart using layer chart switch', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); @@ -203,7 +272,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should allow seamless transition from pie chart to treemap chart', async () => { + it('should transition from pie chart to treemap chart', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsPieVis'); await PageObjects.lens.clickVisualizeListItemTitle('lnsPieVis'); @@ -221,7 +290,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should allow creating a pie chart and switching to datatable', async () => { + it('should create a pie chart and switch to datatable', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); @@ -231,6 +300,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'date_histogram', field: '@timestamp', }); + await PageObjects.lens.closeDimensionEditor(); await PageObjects.lens.configureDimension({ dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension', @@ -238,6 +308,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'bytes', }); + await PageObjects.lens.closeDimensionEditor(); expect(await PageObjects.lens.hasChartSwitchWarning('lnsDatatable')).to.eql(false); await PageObjects.lens.switchToVisualization('lnsDatatable'); diff --git a/x-pack/test/functional/config_security_basic.ts b/x-pack/test/functional/config_security_basic.ts index 48c397d9c37de..968281b75b7ac 100644 --- a/x-pack/test/functional/config_security_basic.ts +++ b/x-pack/test/functional/config_security_basic.ts @@ -70,7 +70,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, junit: { - reportName: 'Chrome X-Pack UI Functional Tests', + reportName: 'Chrome X-Pack UI Functional Tests (Security Basic)', }, }; } diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz index 58ac5616651d4..7736287bc9a37 100644 Binary files a/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz and b/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz index 06e83f8c267d6..0cec8a44dea8d 100644 Binary files a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz and b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz differ diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index e3c21085b92d3..a1e62afbe14c8 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -13,7 +13,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const retry = getService('retry'); const find = getService('find'); const comboBox = getService('comboBox'); - const PageObjects = getPageObjects(['header', 'header', 'timePicker']); + const PageObjects = getPageObjects(['header', 'header', 'timePicker', 'common']); return logWrapper('lensPage', log, { /** @@ -85,19 +85,32 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param layerIndex - the index of the layer */ async configureDimension( - opts: { dimension: string; operation: string; field: string }, + opts: { + dimension: string; + operation: string; + field?: string; + isPreviousIncompatible?: boolean; + }, layerIndex = 0 ) { await retry.try(async () => { await testSubjects.click(`lns-layerPanel-${layerIndex} > ${opts.dimension}`); await testSubjects.exists(`lns-indexPatternDimension-${opts.operation}`); }); + const operationSelector = opts.isPreviousIncompatible + ? `lns-indexPatternDimension-${opts.operation} incompatible` + : `lns-indexPatternDimension-${opts.operation}`; + await testSubjects.click(operationSelector); + + if (opts.field) { + const target = await testSubjects.find('indexPattern-dimension-field'); + await comboBox.openOptionsList(target); + await comboBox.setElement(target, opts.field); + } + }, - await testSubjects.click(`lns-indexPatternDimension-${opts.operation}`); - - const target = await testSubjects.find('indexPattern-dimension-field'); - await comboBox.openOptionsList(target); - await comboBox.setElement(target, opts.field); + // closes the dimension editor flyout + async closeDimensionEditor() { await testSubjects.click('lns-indexPattern-dimensionContainerTitle'); }, @@ -107,7 +120,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont async removeDimension(dimensionTestSubj: string) { await testSubjects.click(`${dimensionTestSubj} > indexPattern-dimension-remove`); }, - + /** + * adds new filter to filters agg + */ + async addFilterToAgg(queryString: string) { + await testSubjects.click('lns-newBucket-add'); + const queryInput = await testSubjects.find('indexPattern-filters-queryStringInput'); + await queryInput.type(queryString); + await PageObjects.common.pressEnterKey(); + await PageObjects.common.pressEnterKey(); + await PageObjects.common.sleep(1000); // give time for debounced components to rerender + }, /** * Save the current Lens visualization. */ @@ -141,10 +164,43 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('lnsApp_saveAndReturnButton'); }, + async editDimensionLabel(label: string) { + await testSubjects.setValue('indexPattern-label-edit', label); + }, + async editDimensionFormat(format: string) { + const formatInput = await testSubjects.find('indexPattern-dimension-format'); + await comboBox.openOptionsList(formatInput); + await comboBox.setElement(formatInput, format); + }, + async editDimensionColor(color: string) { + const colorPickerInput = await testSubjects.find('colorPickerAnchor'); + await colorPickerInput.type(color); + await PageObjects.common.sleep(1000); // give time for debounced components to rerender + }, + async editMissingValues(option: string) { + await retry.try(async () => { + await testSubjects.click('lnsMissingValuesButton'); + await testSubjects.exists('lnsMissingValuesSelect'); + }); + await testSubjects.click('lnsMissingValuesSelect'); + const optionSelector = await find.byCssSelector(`#${option}`); + await optionSelector.click(); + }, + getTitle() { return testSubjects.getVisibleText('lns_ChartTitle'); }, + async getFiltersAggLabels() { + const labels = []; + const filters = await testSubjects.findAll('indexPattern-filters-existingFilterContainer'); + for (let i = 0; i < filters.length; i++) { + labels.push(await filters[i].getVisibleText()); + } + log.debug(`Found ${labels.length} filters on current page`); + return labels; + }, + /** * Uses the Lens visualization switcher to switch visualizations. * @@ -275,5 +331,13 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await this.assertExactText('[data-test-subj="lns_metric_title"]', title); await this.assertExactText('[data-test-subj="lns_metric_value"]', count); }, + + async assertMissingValues(option: string) { + await this.assertExactText('[data-test-subj="lnsMissingValuesSelect"]', option); + }, + async assertColor(color: string) { + // TODO: target dimensionTrigger color element after merging https://github.com/elastic/kibana/pull/76871 + await testSubjects.getAttribute('colorPickerAnchor', color); + }, }); } diff --git a/x-pack/test/functional/page_objects/space_selector_page.ts b/x-pack/test/functional/page_objects/space_selector_page.ts index acf8a65362f01..426f8e520815e 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.ts +++ b/x-pack/test/functional/page_objects/space_selector_page.ts @@ -66,8 +66,8 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro await testSubjects.setValue('addSpaceName', spaceName); } - async clickSpaceAcustomAvatar() { - await testSubjects.click('space-avatar-space_a'); + async clickCustomizeSpaceAvatar(spaceId: string) { + await testSubjects.click(`space-avatar-${spaceId}`); } async clickSpaceInitials() { @@ -122,10 +122,6 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro await testSubjects.setValue('spaceURLDisplay', spaceURL); } - async clickFeaturesVisibilityButton() { - await testSubjects.click('changeAllFeatureVisibilityPopover'); - } - async clickHideAllFeatures() { await testSubjects.click('spc-toggle-all-features-hide'); } @@ -134,8 +130,28 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro await testSubjects.click('spc-toggle-all-features-show'); } - async toggleFeatureVisibility(featureName: string) { - await testSubjects.click(`feature-${featureName}-toggle`); + async openFeatureCategory(categoryName: string) { + const category = await find.byCssSelector( + `button[aria-controls=featureCategory_${categoryName}]` + ); + const isCategoryExpanded = (await category.getAttribute('aria-expanded')) === 'true'; + if (!isCategoryExpanded) { + await category.click(); + } + } + + async closeFeatureCategory(categoryName: string) { + const category = await find.byCssSelector( + `button[aria-controls=featureCategory_${categoryName}]` + ); + const isCategoryExpanded = (await category.getAttribute('aria-expanded')) === 'true'; + if (isCategoryExpanded) { + await category.click(); + } + } + + async toggleFeatureCategoryVisibility(categoryName: string) { + await testSubjects.click(`featureCategoryButton_${categoryName}`); } async clickOnDescriptionOfSpace() { diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index 5c42c1978a0b5..fd7869eac918f 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -25,6 +25,7 @@ export class AlertingFixturePlugin implements Plugin { - describe('Saved Search Features', () => { - after(async () => { - await reportingAPI.deleteAllReports(); - }); + after(async () => { + await reportingAPI.deleteAllReports(); + }); + describe('Saved Search Features', () => { it('With filters and timebased data, explicit UTC format', async () => { // load test data that contains a saved search and documents await esArchiver.load('reporting/logs'); @@ -350,8 +350,8 @@ export default function ({ getService }: FtrProviderContext) { searchId: 'search:6091ead0-1c6d-11ea-a100-8589bb9d7c6b', postPayload: { timerange: { - min: '2019-06-26T06:20:28Z', - max: '2019-06-26T07:27:58Z', + min: '2019-05-28T00:00:00Z', + max: '2019-06-26T00:00:00Z', timezone: 'UTC', }, state: { @@ -370,8 +370,8 @@ export default function ({ getService }: FtrProviderContext) { { range: { order_date: { - gte: '2019-06-26T06:20:28.066Z', - lte: '2019-06-26T07:27:58.573Z', + gte: '2019-05-28T00:00:00.000Z', + lte: '2019-06-26T00:00:00.000Z', format: 'strict_date_optional_time', }, }, diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts index 9eafd0c318383..3f2b2e7116206 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts @@ -12,51 +12,106 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const reportingAPI = getService('reportingAPI'); const supertest = getService('supertest'); const log = getService('log'); + const setSpaceConfig = async (spaceId: string, settings: object) => { + return await kibanaServer.request({ + path: `/s/${spaceId}/api/kibana/settings`, + method: 'POST', + body: { changes: settings }, + }); + }; + const getCompleted$ = (downloadPath: string) => { return Rx.interval(2000).pipe( tap(() => log.debug(`checking report status at ${downloadPath}...`)), switchMap(() => supertest.get(downloadPath)), filter(({ status: statusCode }) => statusCode === 200), + tap(() => log.debug(`report at ${downloadPath} is done`)), map((response) => response.text), first(), timeout(15000) ); }; - describe('Exports from Non-default Space', () => { + describe('Exports and Spaces', () => { before(async () => { await esArchiver.load('reporting/ecommerce'); - await esArchiver.load('reporting/ecommerce_kibana_spaces'); // dashboard in non default space + await esArchiver.load('reporting/ecommerce_kibana_spaces'); // multiple spaces with different config settings }); after(async () => { await esArchiver.unload('reporting/ecommerce'); await esArchiver.unload('reporting/ecommerce_kibana_spaces'); - }); - - afterEach(async () => { await reportingAPI.deleteAllReports(); }); - it('should complete a job of CSV saved search export in non-default space', async () => { - const downloadPath = await reportingAPI.postJob( - `/s/non_default_space/api/reporting/generate/csv?jobParams=%28browserTimezone%3AUTC%2CconflictedTypesFields%3A%21%28%29%2Cfields%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%2CindexPatternId%3A%27067dec90-e7ee-11ea-a730-d58e9ea7581b%27%2CmetaFields%3A%21%28_source%2C_id%2C_type%2C_index%2C_score%29%2CobjectType%3Asearch%2CsearchRequest%3A%28body%3A%28_source%3A%28includes%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%29%2Cdocvalue_fields%3A%21%28%28field%3Aorder_date%2Cformat%3Adate_time%29%29%2Cquery%3A%28bool%3A%28filter%3A%21%28%28match_all%3A%28%29%29%2C%28range%3A%28order_date%3A%28format%3Astrict_date_optional_time%2Cgte%3A%272019-06-11T08%3A24%3A16.425Z%27%2Clte%3A%272019-07-13T09%3A31%3A07.520Z%27%29%29%29%29%2Cmust%3A%21%28%29%2Cmust_not%3A%21%28%29%2Cshould%3A%21%28%29%29%29%2Cscript_fields%3A%28%29%2Csort%3A%21%28%28order_date%3A%28order%3Adesc%2Cunmapped_type%3Aboolean%29%29%29%2Cstored_fields%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%2Cversion%3A%21t%29%2Cindex%3A%27ecommerce%2A%27%29%2Ctitle%3A%27Ecom%20Search%27%29` - ); + describe('CSV saved search export', () => { + it('should use formats from the default space', async () => { + kibanaServer.uiSettings.update({ 'csv:separator': ',', 'dateFormat:tz': 'UTC' }); + const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv`, { + jobParams: `(conflictedTypesFields:!(),fields:!(order_date,order_date,customer_full_name,taxful_total_price),indexPatternId:aac3e500-f2c7-11ea-8250-fb138aa491e7,metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,customer_full_name,taxful_total_price)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-11T04:49:43.495Z',lte:'2019-07-14T10:25:34.149Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,customer_full_name,taxful_total_price),version:!t),index:'ec*'),title:'EC SEARCH')`, + }); + const csv = await getCompleted$(path).toPromise(); + expect(csv).to.match( + /^"order_date","order_date","customer_full_name","taxful_total_price"\n"Jul 12, 2019 @ 00:00:00.000","Jul 12, 2019 @ 00:00:00.000","Sultan Al Boone","173.96"/ + ); + }); - // Retry the download URL until a "completed" response status is returned - const completed$ = getCompleted$(downloadPath); - const reportCompleted = await completed$.toPromise(); - expect(reportCompleted).to.match(/^"order_date",/); + it('should use formats from non-default spaces', async () => { + setSpaceConfig('non_default_space', { + 'csv:separator': ';', + 'csv:quoteValues': false, + 'dateFormat:tz': 'US/Alaska', + }); + const path = await reportingAPI.postJobJSON( + `/s/non_default_space/api/reporting/generate/csv`, + { + jobParams: `(conflictedTypesFields:!(),fields:!(order_date,category,customer_first_name,customer_full_name,total_quantity,total_unique_products,taxless_total_price,taxful_total_price,currency),indexPatternId:'067dec90-e7ee-11ea-a730-d58e9ea7581b',metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,category,customer_first_name,customer_full_name,total_quantity,total_unique_products,taxless_total_price,taxful_total_price,currency)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-11T08:24:16.425Z',lte:'2019-07-13T09:31:07.520Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,category,customer_first_name,customer_full_name,total_quantity,total_unique_products,taxless_total_price,taxful_total_price,currency),version:!t),index:'ecommerce*'),title:'Ecom Search')`, + } + ); + const csv = await getCompleted$(path).toPromise(); + expect(csv).to.match( + /^order_date;category;customer_first_name;customer_full_name;total_quantity;total_unique_products;taxless_total_price;taxful_total_price;currency\nJul 11, 2019 @ 16:00:00.000;/ + ); + }); + + it(`should use browserTimezone in jobParams for date formatting`, async () => { + const tzParam = 'America/Phoenix'; + const tzSettings = 'Browser'; + setSpaceConfig('non_default_space', { 'csv:separator': ';', 'dateFormat:tz': tzSettings }); + const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv`, { + jobParams: `(browserTimezone:${tzParam},conflictedTypesFields:!(),fields:!(order_date,category,customer_full_name,taxful_total_price,currency),indexPatternId:aac3e500-f2c7-11ea-8250-fb138aa491e7,metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,category,customer_full_name,taxful_total_price,currency)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-05-30T05:09:59.743Z',lte:'2019-07-26T08:47:09.682Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,category,customer_full_name,taxful_total_price,currency),version:!t),index:'ec*'),title:'EC SEARCH from DEFAULT')`, + }); + + const csv = await getCompleted$(path).toPromise(); + expect(csv).to.match( + /^"order_date",category,"customer_full_name","taxful_total_price",currency\n"Jul 11, 2019 @ 17:00:00.000"/ + ); + }); + + it(`should default to UTC for date formatting when timezone is not known`, async () => { + kibanaServer.uiSettings.update({ 'csv:separator': ',', 'dateFormat:tz': 'Browser' }); + const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv`, { + jobParams: `(conflictedTypesFields:!(),fields:!(order_date,order_date,customer_full_name,taxful_total_price),indexPatternId:aac3e500-f2c7-11ea-8250-fb138aa491e7,metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,customer_full_name,taxful_total_price)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-11T04:49:43.495Z',lte:'2019-07-14T10:25:34.149Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,customer_full_name,taxful_total_price),version:!t),index:'ec*'),title:'EC SEARCH')`, + }); + const csv = await getCompleted$(path).toPromise(); + expect(csv).to.match( + /^"order_date","order_date","customer_full_name","taxful_total_price"\n"Jul 12, 2019 @ 00:00:00.000","Jul 12, 2019 @ 00:00:00.000","Sultan Al Boone","173.96"/ + ); + }); }); // FLAKY: https://github.com/elastic/kibana/issues/76551 it.skip('should complete a job of PNG export of a dashboard in non-default space', async () => { - const downloadPath = await reportingAPI.postJob( - `/s/non_default_space/api/reporting/generate/png?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apng%29%2CobjectType%3Adashboard%2CrelativeUrl%3A%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29` + const downloadPath = await reportingAPI.postJobJSON( + `/s/non_default_space/api/reporting/generate/png`, + { + jobParams: `(browserTimezone:UTC,layout:(dimensions:(height:512,width:2402),id:png),objectType:dashboard,relativeUrl:'/s/non_default_space/app/dashboards#/view/3c9ee360-e7ee-11ea-a730-d58e9ea7581b?_g=(filters:!!(),refreshInterval:(pause:!!t,value:0),time:(from:!'2019-06-10T03:17:28.800Z!',to:!'2019-07-14T19:25:06.385Z!'))&_a=(description:!'!',filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),query:(language:kuery,query:!'!'),timeRestore:!!t,title:!'Ecom%20Dashboard%20Non%20Default%20Space!',viewMode:view)',title:'Ecom Dashboard Non Default Space')`, + } ); const completed$: Rx.Observable = getCompleted$(downloadPath); @@ -66,8 +121,11 @@ export default function ({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/76551 it.skip('should complete a job of PDF export of a dashboard in non-default space', async () => { - const downloadPath = await reportingAPI.postJob( - `/s/non_default_space/api/reporting/generate/printablePdf?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apreserve_layout%29%2CobjectType%3Adashboard%2CrelativeUrls%3A%21%28%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%29%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29` + const downloadPath = await reportingAPI.postJobJSON( + `/s/non_default_space/api/reporting/generate/printablePdf`, + { + jobParams: `(browserTimezone:UTC,layout:(dimensions:(height:512,width:2402),id:preserve_layout),objectType:dashboard,relativeUrls:!('/s/non_default_space/app/dashboards#/view/3c9ee360-e7ee-11ea-a730-d58e9ea7581b?_g=(filters:!!(),refreshInterval:(pause:!!t,value:0),time:(from:!'2019-06-10T03:17:28.800Z!',to:!'2019-07-14T19:25:06.385Z!'))&_a=(description:!'!',filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),query:(language:kuery,query:!'!'),timeRestore:!!t,title:!'Ecom%20Dashboard%20Non%20Default%20Space!',viewMode:view)'),title:'Ecom Dashboard Non Default Space')`, + } ); const completed$ = getCompleted$(downloadPath); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts index aaf4dd3926411..99a46684d8a67 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts @@ -115,8 +115,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - // FAILING: https://github.com/elastic/kibana/issues/76581 - describe.skip('from new jobs posted', () => { + describe('from new jobs posted', () => { it('should handle csv', async () => { await reportingAPI.expectAllJobsToFinishSuccessfully( await Promise.all([ @@ -133,7 +132,8 @@ export default function ({ getService }: FtrProviderContext) { reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 0); }); - it('should handle preserve_layout pdf', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/76581 + it.skip('should handle preserve_layout pdf', async () => { await reportingAPI.expectAllJobsToFinishSuccessfully( await Promise.all([ reportingAPI.postJob(GenerationUrls.PDF_PRESERVE_DASHBOARD_FILTER_6_3), @@ -150,7 +150,8 @@ export default function ({ getService }: FtrProviderContext) { reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2); }); - it('should handle print_layout pdf', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/76581 + it.skip('should handle print_layout pdf', async () => { await reportingAPI.expectAllJobsToFinishSuccessfully( await Promise.all([ reportingAPI.postJob(GenerationUrls.PDF_PRINT_DASHBOARD_6_3), diff --git a/x-pack/test/reporting_api_integration/services.ts b/x-pack/test/reporting_api_integration/services.ts index e61e6483855af..2c0252fde7693 100644 --- a/x-pack/test/reporting_api_integration/services.ts +++ b/x-pack/test/reporting_api_integration/services.ts @@ -84,7 +84,7 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { ); }, - async postJob(apiPath: string) { + async postJob(apiPath: string): Promise { log.debug(`ReportingAPI.postJob(${apiPath})`); const { body } = await supertest .post(removeWhitespace(apiPath)) @@ -93,6 +93,12 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) { return body.path; }, + async postJobJSON(apiPath: string, jobJSON: object = {}): Promise { + log.debug(`ReportingAPI.postJobJSON((${apiPath}): ${JSON.stringify(jobJSON)})`); + const { body } = await supertest.post(apiPath).set('kbn-xsrf', 'xxx').send(jobJSON); + return body.path; + }, + /** * * @return {Promise} A function to call to clean up the index alias that was added. diff --git a/x-pack/test/security_api_integration/session_idle.config.ts b/x-pack/test/security_api_integration/session_idle.config.ts index da85c6342037e..34a23b7f5f926 100644 --- a/x-pack/test/security_api_integration/session_idle.config.ts +++ b/x-pack/test/security_api_integration/session_idle.config.ts @@ -36,7 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, junit: { - reportName: 'X-Pack Security API Integration Tests', + reportName: 'X-Pack Security API Integration Tests (Session Idle Timeout)', }, }; } diff --git a/x-pack/test/security_api_integration/session_lifespan.config.ts b/x-pack/test/security_api_integration/session_lifespan.config.ts index 17773a7739847..b5fdf6b6914b1 100644 --- a/x-pack/test/security_api_integration/session_lifespan.config.ts +++ b/x-pack/test/security_api_integration/session_lifespan.config.ts @@ -36,7 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, junit: { - reportName: 'X-Pack Security API Integration Tests', + reportName: 'X-Pack Security API Integration Tests (Session Lifespan)', }, }; } diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index f288bc925123e..01e2ad76fb3d2 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -33,15 +33,11 @@ export default function ({ getService }: FtrProviderContext) { return (await es.search({ index: '.kibana_security_session*' })).hits.total.value; } - // FLAKY: https://github.com/elastic/kibana/issues/76239 - describe.skip('Session Idle cleanup', () => { + describe('Session Idle cleanup', () => { beforeEach(async () => { await es.cluster.health({ index: '.kibana_security_session*', waitForStatus: 'green' }); - await es.deleteByQuery({ + await es.indices.delete({ index: '.kibana_security_session*', - q: '*', - waitForCompletion: true, - refresh: true, ignore: [404], }); }); diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts index dbdaf494fdf27..6036acf3d1cf1 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts @@ -30,15 +30,11 @@ export default function ({ getService }: FtrProviderContext) { return (await es.search({ index: '.kibana_security_session*' })).hits.total.value; } - // FLAKY: https://github.com/elastic/kibana/issues/76223 - describe.skip('Session Lifespan cleanup', () => { + describe('Session Lifespan cleanup', () => { beforeEach(async () => { await es.cluster.health({ index: '.kibana_security_session*', waitForStatus: 'green' }); - await es.deleteByQuery({ + await es.indices.delete({ index: '.kibana_security_session*', - q: '*', - waitForCompletion: true, - refresh: true, ignore: [404], }); }); diff --git a/x-pack/test/security_functional/login_selector.config.ts b/x-pack/test/security_functional/login_selector.config.ts index 48665c93c091a..bdb4778740503 100644 --- a/x-pack/test/security_functional/login_selector.config.ts +++ b/x-pack/test/security_functional/login_selector.config.ts @@ -76,7 +76,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { screenshots: { directory: resolve(__dirname, 'screenshots') }, junit: { - reportName: 'Chrome X-Pack Security Functional Tests', + reportName: 'Chrome X-Pack Security Functional Tests (Login Selector)', }, }; } diff --git a/x-pack/test/security_functional/oidc.config.ts b/x-pack/test/security_functional/oidc.config.ts index 5fd59e049a0f4..1ed5d51098420 100644 --- a/x-pack/test/security_functional/oidc.config.ts +++ b/x-pack/test/security_functional/oidc.config.ts @@ -76,7 +76,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { screenshots: { directory: resolve(__dirname, 'screenshots') }, junit: { - reportName: 'Chrome X-Pack Security Functional Tests', + reportName: 'Chrome X-Pack Security Functional Tests (OpenID Connect)', }, }; } diff --git a/x-pack/test/security_functional/saml.config.ts b/x-pack/test/security_functional/saml.config.ts index c47145f8bc039..9d925bee480a8 100644 --- a/x-pack/test/security_functional/saml.config.ts +++ b/x-pack/test/security_functional/saml.config.ts @@ -70,7 +70,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { screenshots: { directory: resolve(__dirname, 'screenshots') }, junit: { - reportName: 'Chrome X-Pack Security Functional Tests', + reportName: 'Chrome X-Pack Security Functional Tests (SAML)', }, }; } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts index e8fa18aa01b4c..620eab37f9b46 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts @@ -13,8 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const queryBar = getService('queryBar'); - // FLAKY: https://github.com/elastic/kibana/issues/77835 - describe.skip('Endpoint Event Resolver', function () { + describe('Endpoint Event Resolver', function () { before(async () => { await esArchiver.load('endpoint/resolver_tree', { useCreate: true }); await pageObjects.hosts.navigateToSecurityHostsPage(); @@ -45,7 +44,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const nodeData: string[] = []; const TableData: string[] = []; - const Table = await testSubjects.findAll('resolver:node-list:item'); + const Table = await testSubjects.findAll('resolver:node-list:node-link:title'); for (const value of Table) { const text = await value._webElement.getText(); TableData.push(text.split('\n')[0]); diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts index a950b4fc3d70a..c5d188c4139bf 100644 --- a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts @@ -18,6 +18,7 @@ class FooPlugin implements Plugin { id: 'foo', name: 'Foo', icon: 'upArrow', + category: { id: 'foo', label: 'foo' }, navLinkId: 'foo_plugin', app: ['foo_plugin', 'kibana'], catalogue: ['foo'], diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index dde99e7409dee..a3bacffffd54d 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -13,7 +13,15 @@ import { UserAtSpaceScenarios } from '../scenarios'; export default function catalogueTests({ getService }: FtrProviderContext) { const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); - const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher']; + const esFeatureExceptions = [ + 'security', + 'index_lifecycle_management', + 'snapshot_restore', + 'rollup_jobs', + 'reporting', + 'transform', + 'watcher', + ]; describe('catalogue', () => { UserAtSpaceScenarios.forEach((scenario) => { diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts index 1f19228b2d958..ad54efca91272 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -13,7 +13,15 @@ import { UserScenarios } from '../scenarios'; export default function catalogueTests({ getService }: FtrProviderContext) { const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); - const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher']; + const esFeatureExceptions = [ + 'security', + 'index_lifecycle_management', + 'snapshot_restore', + 'rollup_jobs', + 'reporting', + 'transform', + 'watcher', + ]; describe('catalogue', () => { UserScenarios.forEach((scenario) => { diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts index baae3286ddb5d..1ef520d179804 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts @@ -13,7 +13,15 @@ import { SpaceScenarios } from '../scenarios'; export default function catalogueTests({ getService }: FtrProviderContext) { const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); - const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher']; + const esFeatureExceptions = [ + 'security', + 'index_lifecycle_management', + 'snapshot_restore', + 'rollup_jobs', + 'reporting', + 'transform', + 'watcher', + ]; describe('catalogue', () => { SpaceScenarios.forEach((scenario) => { diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 38851653898a8..44c8449dc5dd0 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -21,22 +21,12 @@ "paths": { "kibana/public": ["src/core/public"], "kibana/server": ["src/core/server"], - "plugins/xpack_main/*": [ - "x-pack/legacy/plugins/xpack_main/public/*" - ], - "plugins/spaces/*": [ - "x-pack/legacy/plugins/spaces/public/*" - ], - "test_utils/*": [ - "x-pack/test_utils/*" - ], - "plugins/*": ["src/legacy/core_plugins/*/public/"], - "fixtures/*": ["src/fixtures/*"], + "plugins/xpack_main/*": ["x-pack/legacy/plugins/xpack_main/public/*"], + "test_utils/*": ["x-pack/test_utils/*"], + "fixtures/*": ["src/fixtures/*"] }, // overhead is too significant - "incremental": false, + "incremental": false }, - "references": [ - { "path": "../src/core/tsconfig.json" } - ] + "references": [{ "path": "../src/core/tsconfig.json" }] } diff --git a/yarn.lock b/yarn.lock index 7fb61b10c1bf7..cec2697f6c15c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1226,10 +1226,10 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@28.4.0": - version "28.4.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-28.4.0.tgz#6b3b6b5e8b602d009410a97c26e22093846f708a" - integrity sha512-4iXo5fNx4qqeI+Tj5EJ0qHRhyi8KTLaqeQJCWg9Vy7N83ap6kp6s7X6D6qYUHqdmOdJH9QZYuYIpRUi3TQEJNg== +"@elastic/eui@29.0.0": + version "29.0.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-29.0.0.tgz#1c8d822c62ad5e29298a3a36f5b02fd9b32a5550" + integrity sha512-YsDjtN/nRA4vvWukg5FDN4iPQgHUVxDwn/JZ1mArCeMe34JwzYJlEkk6Z/+iNbJOZQNHngmV8I2TStcP8k82gg== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" @@ -4388,10 +4388,10 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg== -"@types/prop-types@^15.5.3": - version "15.5.9" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.9.tgz#f2d14df87b0739041bc53a7d75e3d77d726a3ec0" - integrity sha512-Nha5b+jmBI271jdTMwrHiNXM+DvThjHOfyZtMX9kj/c/LUj2xiLHsG/1L3tJ8DjAoQN48cHwUwtqBotjyXaSdQ== +"@types/prop-types@^15.7.3": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== "@types/proper-lockfile@^3.0.1": version "3.0.1" @@ -9590,7 +9590,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-react-class@^15.5.1: +create-react-class@^15.5.1, create-react-class@^15.5.2: version "15.6.3" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg== @@ -9599,15 +9599,6 @@ create-react-class@^15.5.1: loose-envify "^1.3.1" object-assign "^4.1.1" -create-react-class@^15.5.2: - version "15.6.2" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a" - integrity sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co= - dependencies: - fbjs "^0.8.9" - loose-envify "^1.3.1" - object-assign "^4.1.1" - create-react-context@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.1.6.tgz#0f425931d907741127acc6e31acb4f9015dd9fdc" @@ -9941,6 +9932,11 @@ cypress-multi-reporters@^1.2.3: debug "^4.1.1" lodash "^4.17.11" +cypress-promise@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25" + integrity sha512-DhIf5PJ/a0iY+Yii6n7Rbwq+9TJxU4pupXYzf9mZd8nPG0AzQrj9i+pqINv4xbI2EV1p+PKW3maCkR7oPG4GrA== + cypress@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.0.0.tgz#6957e299b790af8b1cd7bea68261b8935646f72e" @@ -22788,15 +22784,6 @@ prop-types-exact@^1.2.0: object.assign "^4.1.0" reflect.ownkeys "^0.2.0" -prop-types@15.6.0: - version "15.6.0" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" - integrity sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY= - dependencies: - fbjs "^0.8.16" - loose-envify "^1.3.1" - object-assign "^4.1.1" - prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -28333,9 +28320,9 @@ typescript@4.0.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, types integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== ua-parser-js@^0.7.18: - version "0.7.21" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" - integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== + version "0.7.22" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3" + integrity sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5"