diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7daa42af7024d5..0bdddddab8de5c 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 @@ -163,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 @@ -281,7 +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 -/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 039b520561d65b..2e8529b4a77046 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 3ff83e9db8c43f..b27e457940d933 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 00000000000000..fe81f7cffaa418 --- /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 00000000000000..79de37ea619f31 --- /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 00000000000000..f0889d200725a2 --- /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 00000000000000..9405118ed7a11b --- /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 00000000000000..a761bf4e5b3937 --- /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 00000000000000..aba1b886076ad7 --- /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 b83c091846f047..be8b7c27495ada 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 bc34d4113f847b..4422b755faa773 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 bfb5dff71e70d9..00000000000000 --- 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 f993721ee96ad3..9212b757e07dfc 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 3385a3773a2aa8..00000000000000 --- 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 083b544493e80e..00000000000000 --- 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 127ee9210799e2..00000000000000 --- 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 a1dffe5ff5fa45..e15b04236a0b53 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 9fcd0310af0feb..00000000000000 --- 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 b428bd9cd90caa..00000000000000 --- 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 69078ca40d20d2..482501e494c7af 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 fb9ebd3c9165f5..00000000000000 --- 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 342acd5e049f10..00000000000000 --- 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 079f352609a70c..00000000000000 --- 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 4e4c49b222f01e..519bbaf8f94168 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 6d11804704d82f..00000000000000 --- 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 942c7930f449d5..00000000000000 --- 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 f5b587d86b3491..3c477e17503f44 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 384fc77d801c02..00000000000000 --- 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 5d61c75bc5e99e..6a7bdfe51f1c0a 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 5c2aa700bc6032..00000000000000 --- 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 b6f40cc2e42038..00000000000000 --- 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 8280d924eb6097..1ac59343220fd7 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 1408fb43cbf39c..00000000000000 --- 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 98aca5474d350c..00000000000000 --- 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/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index 47fbe1bea9f2a6..9d735ea1fe3db6 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 8e246960937ec3..4141b48ffeeaf2 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 + + + + +
+ +
+ +
+
- +
- +
- +
; +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 24d1fc9d369f2d..e136c699f72460 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 1dcf8a22e9cfd4..11a14457784fd1 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 1fb7c284c0dfda..809aaddb741722 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 30e78635ec2e99..017d208133cdc0 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/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts index 28ba0ab629e8f1..317d0f01402931 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 e3099ae6a40264..1e892d452f4013 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 60008ce6054e1b..82b1ae69cc73b5 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 6b34a1baf293bc..c1a98eac5350ea 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/index.ts b/src/plugins/data/common/search/index.ts index 061974d8602464..2ee0db384cf06f 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 6b6cfb0c9b1ca2..dde5983fe73fbe 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 f9987767a9688d..72d93e41305d18 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 5e83e1f57bb6dc..8778eb4fd559db 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 ed87c4813951c0..556fb4924da563 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 00000000000000..1b9a9677e4a991 --- /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 efaaafadf404e3..ba5eb6f2897a94 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 cdf10d8f1a1b06..30055f88012f2b 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 48c0338f7e981b..70c9cfcee2348d 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 07f37c3c112750..81fafc6dcae069 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 b87affdd59c54a..cb6295dd701eee 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 ad59e5c6c96257..3badd456bd72a8 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 7243ab158009ad..81117513917c02 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 1c0573aa895d78..01ffc3876f6af9 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 00000000000000..26587b09ffd9ef --- /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 740bc22a7485c5..1a0a96a76a7032 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 8d9b50d5a66b2a..f271280170166b 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 10004b87ca690a..1899efbf3598d5 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 3ec0a13282d3ee..e41c4482df9c9d 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 d9a9fb2f4fef37..74abd9238bc2bf 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 4afee223454e49..d8a036ce970dda 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 7600bd9db6094b..0a299b57275f87 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 7ce53a219fb443..db8d9dba4e0c79 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 1b2d4765709029..996a7aaa27c314 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 35d2f2b7b294e9..7b303ca4d53147 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 50fbb114b39fd8..1021ef0f91d52c 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 4b8511edfc26fe..340a795d37bfb2 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 c1af9699acbb2b..fc3d71936a8594 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 00000000000000..da39bf521fe3d4 --- /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 fd4f8a07919f83..6b2b9b4da020bd 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 74e516f407e8cc..08e5eab788e767 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 f4ed7d8b122b90..fdd6a900134133 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 c41e1f78ee74e0..d8937ed30e401b 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 83a542269046f2..6ae5d83499aa6b 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 14f176176f647e..c48aa8397dc83a 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) @@ -941,14 +934,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) // @@ -1063,14 +1054,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) // @@ -1120,6 +1108,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 00000000000000..2aab6c2d9093b6 --- /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 00000000000000..0d5dc508e20ad4 --- /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 00000000000000..68b842c934de89 --- /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 6a8e6079232aa6..1ecf76dbbd5c2a 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 57253c1f741abc..c5d8853ada5e8d 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 b22f16c94aff8d..e2047dca1f7708 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 ffe8a5bf6e7dcb..9267d600360cfc 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 7949b6fb8ba27a..a6fa46fbc4e3e6 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 b8985f7311ea9e..224a11a201b888 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 e8aecdba0abc48..3843950c164c96 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 ceaa74218904d0..db71b94ac855fd 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 913c3a0b30826b..d47979b9419f34 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 1cfff7baca1862..7fe189dea2381d 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 2064236e9ae7f6..26c10121adb3d0 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 e37d602ad8cac1..5fd3bcdd613188 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 2ca31994b722d8..00eb923c266624 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 2d112b23598180..c5148bbaefb6bd 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/plugins/embeddable/server/index.ts b/src/plugins/embeddable/server/index.ts new file mode 100644 index 00000000000000..1138478bff4b76 --- /dev/null +++ b/src/plugins/embeddable/server/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EmbeddableServerPlugin, EmbeddableSetup } from './plugin'; + +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 00000000000000..f79c4b76201107 --- /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 00000000000000..64f9325dad3cbb --- /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 1b10756c2975c3..bf1e8c8f0b401a 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 5d00370caf2cce..0e86bf7dd3d84a 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 0dfb4f949f0c73..85f1bc04f353b8 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 4e8441bd64b117..ad92aac67d51b8 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 c2ae2f82eaa462..83572e238bffdf 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 766afb11a87c04..d965042b65cef5 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 1ec5737c5a38b1..e09290c811c7b3 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 00000000000000..ae5e3d514554ce --- /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 3fa5cdc8b5e479..89bce5ae423ee6 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 fafedf46c2bdaa..808578c470ae19 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 0cf6f3723a6397..3442f84599fb82 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 7be18a4774d94b..d3c6ca5d90371d 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 68a153f4272a30..f4241808940b2f 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 a0da8d83bed518..c271888b7c7a4b 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/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index e8f8982d7163cc..c12c633926c1c2 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 ace6a48ed8ff58..87a1bc20920a46 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 d98fa468bd6d19..8bbf6274bd15f9 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 3ac03b444deafb..c0d9a03d02c32a 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 bdd0fbea35fa83..a700781438706a 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/examples/ui_actions_enhanced_examples/.eslintrc.json b/x-pack/examples/ui_actions_enhanced_examples/.eslintrc.json new file mode 100644 index 00000000000000..2aab6c2d9093b6 --- /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 fd782f5468c852..cac5f0b29dc6ec 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 7394690a61eae8..fa2f0825f93357 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 a10e8ad707e972..692de571e8a00c 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 745b4bd72dde8e..cb68004c26d656 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 aec06a4596203a..00000000000000 --- 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: ['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/package.json b/x-pack/package.json index 9a1d424da4a1dc..0560b1bebe42b8 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", @@ -275,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", @@ -354,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/feature.ts b/x-pack/plugins/actions/server/feature.ts index 321509a7b9de6b..abe5921fda7f1a 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/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts index 316bae98bf8c1e..a7c8b940fbf06d 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/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 9515987af8dd97..b3c7ada26c4569 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 026aa0c5238dc5..b13a1c62f66022 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/apm/public/components/app/RumDashboard/CoreVitals/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx index b027609fd3a7f3..0135f0b3695377 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 bb5d37a10fb338..94c3acfaa97274 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 790be81bb65c08..388a8824bc73d1 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/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 1cda70a140c673..14d8e2c3a4d506 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { LicenseType } from '../../licensing/common/types'; import { AlertType } from '../common/alert_types'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { LicensingPluginSetup, LicensingRequestHandlerContext, @@ -15,9 +16,10 @@ import { export const APM_FEATURE = { id: 'apm', name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { - defaultMessage: 'APM', + defaultMessage: 'APM and Client Side Monitoring', }), order: 900, + category: DEFAULT_APP_CATEGORIES.observability, icon: 'apmApp', navLinkId: 'apm', app: ['apm', 'csm', 'kibana'], 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 4fcfb53a058873..2ff0173b9ac12a 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/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 9a41a00883c139..ac5392c9d3dee2 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/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 c21109f8a596ad..330a501a78d391 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/embeddables/embeddable_action_storage.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts index 8881b2063c8db3..e0960b83b23f91 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/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 3d28a05a4b7b48..a9bd03e8f97d4e 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 a600ada554afd4..32a75029567288 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 e89cf06ec86214..aaaeccbd15e724 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 06a3eb158d99d8..c6ec2d52c6d1ae 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 3ff6b1b7bf44fd..4cec44d6fa19a3 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 ee11e0e2bbe2ea..ce6fb548ae6d2d 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 30aa6d07f6b5a2..692a8892031317 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 7532bc0573b08d..f5ba17a632c926 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 7bb9954fa30489..b93e27efccaef8 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 d69c592655fb5a..21c50bf82f4bcf 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/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 3075f9c89eb8d7..84b8fa35cfe9b9 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; @@ -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 e1b4a70cfb1fc4..384391578f0c6d 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 ada7a30a859e08..60a00371e5ade5 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 5ac2f407839e40..f47f30c280b2a5 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 6b102045fa516f..c71a3b6b13338e 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 c26b44dfe8ff8d..73e17537476c89 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 0f2afda663da81..e1615625d605ac 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 40d09dadfe0505..1233e9d2d1357f 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 47900415466b9d..f0f7bca29c99e8 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 83ad08d09de760..dfa03ec9d527dd 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 bb506ecad0ade2..c43a33df2db613 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 4179e82d6ad1de..54b9c4d3fbb172 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 91adbcecaf897d..077e07a89f788e 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 365e430a460fad..50b8f4c6fc40b1 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 835f3e2cde7692..45ec7098aa6395 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 4aa5bd62c05a5e..c7781c2e1d50cb 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 0197ca5396ad11..cd5052b0b49a31 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 960fe3699e2105..c20563dd159138 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 5c768a00783a80..af3ec42ab4ec5d 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 c8befceb4fe322..6428e41b180583 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 2e0ba7cf3efee0..f565321f87ef79 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 3223d0c94178ff..0bc9bba7816ca6 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 5eb0482905e36a..46e39fcdac27a2 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/types/file_datavisualizer.ts b/x-pack/plugins/ml/common/types/file_datavisualizer.ts index a8b775c8d5f609..9dc3896e9be48b 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/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 00000000000000..610b29c85a0627 --- /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 00000000000000..fdfe10c2acf023 --- /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 00000000000000..c37e27e39a7aba --- /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 00000000000000..831ae8de8081a9 --- /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 00000000000000..90b6bbab789f3b --- /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/legacy/plugins/spaces/server/lib/errors.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/types.ts similarity index 57% rename from x-pack/legacy/plugins/spaces/server/lib/errors.ts rename to x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/types.ts index 4d8d71dca7af67..1ec66f5c966610 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/errors.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/types.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { boomify, isBoom } from 'boom'; - -export function wrapError(error: any) { - if (isBoom(error)) { - return error; - } - - return boomify(error, { statusCode: error.status }); +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 00000000000000..17b39f9041ec07 --- /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 00000000000000..5e7de14f451c20 --- /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 a79a7d36f32945..2b49746170f46c 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 1e716824729e33..f6cd5909cbb802 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 36b77a5a25e091..08b61a5fa4eed2 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 d8e0c7274b5495..8feef489fdde1b 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 cf248fcc608965..7224eacf84e902 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 5ab029bfd9f29c..4f0088467dd689 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 cb792fbd6ae039..0b06beabfd24dc 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 f996a49e5eadca..72305f47e71897 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 a0d8ff08525447..794ea9febb5c08 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 ee61d76c8a9330..00000000000000 --- 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 d6f472e18bc7b2..f4e3a7b723c088 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 e0d03eb4864cad..80eaa52d0951b6 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 be18bd7fff361d..d768dc6f8e0843 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 915d5010a4885d..1f3354debc3053 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 e383f21143149c..6ecddae12a9889 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 09e6becc2baec0..00000000000000 --- 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 f420d8b033170d..214157db51cb79 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 3a5deda176b8c1..0ca80581fcc83d 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 9646d7eecd5b56..b387245406fbbf 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 0fc29c5b208d9a..26a4b17aaf71fe 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 173a67ad18edf7..3727b2ec7b432b 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 96e634337e6a94..cae706a479b7f1 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 ec4e54632eef55..8fa8fa5cbe3cba 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 00000000000000..35ab7001ecbe48 --- /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 3020cbb5f28b00..7fd176e71f2d58 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 0aae8b567bcdb1..03d88ca60e2c0a 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 adb89abe202805..6a93a35bfcc845 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 979283f9f037c3..0acf384869dedb 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 6ec35db5caec66..72772f9f7b755d 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 c67a95c2de754f..a3c63a0fb539d5 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/security/public/management/roles/__fixtures__/kibana_features.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts index 2b78355787ff23..1bab51e70a4947 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 bf791b37087bda..7dff2912e6aa3d 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 7ecf32ee45b857..77b6da2a004871 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 bc606133459107..0242fddc957c92 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 98faae6edab2cd..ea24560c8ddc9b 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 dc261e2eec9826..5f19c911fd5d38 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 033040fd2f14b5..bdf2c87f40f0bd 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 dd8ac44386dbd7..6f721c91fbd67a 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 8e6d72670c8d99..d449eb29d53d8f 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 d91a4d41513162..0c7d12f67f4b9b 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 6e9b88f30479f7..811ea080b43166 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 f002e13a07cf18..5fbba84467ecf4 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 b666b95ea1e976..777256ff961f90 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 9b90cf9fdf7828..69538f025d95dd 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 137b40eb648baa..8c916e4f013b42 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 f1e87bdb11e75f..32299be500b457 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 56bc68a275ee47..19517017743f15 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 7b6b98383cc338..c024ba1c48f8d8 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 43f0901912271c..75ad92578318ce 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 91b14fa9b999c2..c2a41005ebf4d6 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 e3b4196c90c6c9..160dbb92b74cd2 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 0515bee0052d78..bd25a63e153dde 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 698716fea696e8..b63d70783b7b52 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 ed9fb8930ea1bb..a462b297d37f84 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 36fc063761840f..3a47d4af6ac145 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 c26a7efb0c2882..1c2254f9f8f099 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 00000000000000..63d593ea84e67f --- /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 00000000000000..f2b4754c2d113c --- /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 99680ffe41d444..ea50acc9b46be5 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 170d28cb5a725a..f76417099bb173 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/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 073cb46d3949a3..f2eb5cf5b94f38 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 0c866ae0bd926d..4d46d4dc868467 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 124ef9e6484034..d714f04f519d4e 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 2acb3e57c5a3b9..65a5c6aca00508 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 dbfb0333e48ee2..42fbe40d690ea7 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 c0c9cdf227643f..218cef36ed50a4 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 db4b514a6c748b..00000000000000 --- 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 935e565be039e6..dba1136193ee1a 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 fcc363a1560d5e..53b889004798f5 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 00000000000000..d0340160eb5393 --- /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 610deef07775bc..75aecf6747cca2 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 deddd171982298..4e9d64f5a76a45 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 fd564cde9d15cd..6113cea4c4edc1 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 19f0aa3fe1d678..a7d76277c6ab1d 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 3edfe36087e688..65ec395080f864 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 f96d89b893cebe..13dcfcabe50cb7 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 dda90df0fff936..d40aa0b26a94b3 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 00000000000000..edf551c6cbeb9a --- /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 00000000000000..8072266f1e8c8a --- /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 00000000000000..c743ebc43f2bed --- /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 00000000000000..0336a29bb0721f --- /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 00000000000000..0e1fd5737a3ce5 --- /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/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 29c56e8ed80b1e..fb01f922555168 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 959bf3186f1365..dd887233c36a31 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 701e5b5e706ed3..26ab89ad8ea7cd 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 0f44b50d4bc747..0f5d0304f5ca02 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 11f74c264ae0cf..2159245f2f7350 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 ee83ea91578c5b..556ea209152e6c 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 1117f34b6f8c5a..95067e57868d14 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 0c67d9ca77146f..9ed94cd7bff2e6 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 3af0c3f55b485b..59e14dcffc3c01 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 b845990fd94ef9..6b851351f27f25 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 00000000000000..23c1914387c44d --- /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 00000000000000..b0ec2973b2dd96 --- /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 00000000000000..85eac94a2991f2 --- /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 00000000000000..c914e568048a17 --- /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 9d3eb29be08dde..bbdb8ea0a36ed2 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 6323938d6903bf..6ce0be54a9e7b6 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 0cf0c3880fc98f..68c6a51b4e6f66 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 be1c44de593a49..756aedd5273d39 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 0c56ed300cb483..c8f8341392553e 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 d08ca90f3e3534..dbb48d59d3a3f9 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 b8311182f3ca8c..3ff5d5d2a6e132 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 d48b5b434c9c0b..196c17b42221b2 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 41c825ea4d9787..374b967d1e77f0 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 be71c67615a4c6..e8f254e6a8966b 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 00000000000000..b1fab34d66ab8c --- /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 00000000000000..cf4a570248c99f --- /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 00000000000000..3299b6ae34e4d5 --- /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 00000000000000..7542128d837698 --- /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 00000000000000..9027475d71c4a4 --- /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 00000000000000..f600463c213c21 --- /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 00000000000000..8b381ca0d96dcc --- /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 00000000000000..4c3cd9943adb47 --- /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 00000000000000..48bdf430b940e7 --- /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 00000000000000..38bbb70b6c4ec4 --- /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 700a8fb5022d72..23aa786558a996 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 9d22ba9dcc02b3..123b9c9bdffa2a 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 4a6ea96e1854bb..9f1e5d69804660 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 cbe756064b72b2..b0554adcc46b0f 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 f0e7372a208fbe..0571c4878956f3 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 c9f8431fe1ab76..9500810a395f87 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 3835fa085c26e6..ee1eb7c5e9aba7 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 0eed6793ddbe00..4b22b92cfee16a 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 689bb610d5f38c..5e7629c29bbdd0 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 00000000000000..4f73349edac205 --- /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 9265ca46e3a3a4..95ff475ef4e30c 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 f5807208488753..66f5ea87551d3f 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 5338710b7c8a44..6943e275015547 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 2d1ec727b3348b..d9ad63c30adde1 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 38bf3519020967..f3a2273d90e8c2 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 fe4bdc865094f8..c1d19eb06c2e75 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 1e8520a2617dd3..e345657a785c12 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 5b8b993d96adc0..a328c50af4e7a6 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/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 06fbed44438ffd..11acd28c1795ec 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8677,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時間", @@ -13937,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}", @@ -17539,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": "素晴らしいスペース", @@ -17553,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 80d4f550a10155..41075a0faa081e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8683,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": "上一小时", @@ -13946,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}", @@ -17549,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": "超卓的空间", @@ -17563,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 6177262557e076..c69c33c0fe22ee 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 @@ -248,6 +248,7 @@ export const AlertForm = ({ { 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 00000000000000..2aab6c2d9093b6 --- /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 00000000000000..1150f4f823e8e4 --- /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 108c66505f25c6..5435019f216f2a 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.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index a3f6cac3ba1b48..ca7f6af4f7a374 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 @@ -31,7 +31,7 @@ import { 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< @@ -57,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 @@ -219,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[]; 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 71286e9a59c06e..af930bfba6b8b6 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 f7284539ab2fee..daa56354289cf7 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 2412cdd51748c0..c4b07fa05c3c18 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 6f9eccde8bdb07..28a0990cf75262 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 58c36e36481b88..78eec05eb2d0bd 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 8f73c2b3b3cc97..2f5f7760d40bdf 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 d7f94a52088b7f..45655c2634fe74 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 f5e565d4090ff0..b55b4b87ebccd2 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,13 +30,13 @@ 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. */ 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 31c7481c9d63eb..fb7d96aaf8325f 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.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 3ad6d4ee397493..57c8733ed44fcc 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, @@ -121,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 7ec6b21485747b..b4df1f827a2a3f 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 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 00000000000000..7cac49624bfddc --- /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 39d9dfeca2fd6c..83232bbce1ba7b 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 6ca388281ad769..471b929fdbc068 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 d00db0d9acb7a2..28d104093f64f7 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 4a899b24852a96..ae720598ec7599 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 17a6fc1b955dff..9eb0a06b6dbaf0 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 b38bc44abe2b06..b05c08c4c77d02 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 3a0b65d2ed8446..6c71868222b242 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 ab0aa1200f5a78..5e40d803962de2 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[]; @@ -102,6 +111,9 @@ export class UiActionsServiceEnhancements { licenseFeatureName, supportedTriggers, isCompatible, + telemetry, + extract, + inject, }: DrilldownDefinition): void => { const actionFactory: ActionFactoryDefinition< Config, @@ -119,6 +131,9 @@ export class UiActionsServiceEnhancements { isConfigValid, getDisplayName, supportedTriggers, + telemetry, + extract, + inject, getIconType: () => euiIcon, isCompatible: async () => true, create: (serializedAction) => ({ @@ -151,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 3d143b0cacd063..9a529f192158d6 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 00000000000000..b3664362009149 --- /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 00000000000000..5419c4135796df --- /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 00000000000000..0a61c917a2c5ca --- /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 00000000000000..4859be67283442 --- /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/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 5c3211eff3b4ed..cd2dc5018e110a 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 e11de1376e4000..f4553e4c3a6fe7 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 68ff3dad9ae866..43e4f642bb943a 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 { + // 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/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts index 18beb76e5a3a0b..6364a79a12f045 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/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js index bd35374643e9b8..b74df717010263 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 00000000000000..6074bba372cb23 --- /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 00000000000000..70185d511f2c19 --- /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 00000000000000..1e14411ffc2b0f --- /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 4a68c9a8ff3f2a..fa13d013ea1157 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 4974b63be6f72c..751fbbce13addd 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 f6882c8aed2144..8e1dc231b6b1a9 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 8c4321d77acf46..42807a23cb13ab 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/es_archives/reporting/ecommerce/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz index 58ac5616651d41..7736287bc9a37e 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 06e83f8c267d66..0cec8a44dea8d9 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 e3c21085b92d31..a1e62afbe14c80 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 acf8a65362f01b..426f8e520815e0 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 5c42c1978a0b5c..fd7869eac918ff 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 9eafd0c318383b..3f2b2e7116206e 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 aaf4dd39264114..99a46684d8a67a 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 e61e6483855af8..2c0252fde76938 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_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts index e8fa18aa01b4cd..620eab37f9b46e 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 a950b4fc3d70ab..c5d188c4139bf9 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 dde99e7409dee3..a3bacffffd54d9 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 1f19228b2d9582..ad54efca912723 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 baae3286ddb5d9..1ef520d179804f 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 0f5a7526d98bcc..44c8449dc5dd0f 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,7 +22,6 @@ "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/*"], "fixtures/*": ["src/fixtures/*"] }, diff --git a/yarn.lock b/yarn.lock index fa5f6ff1356701..cec2697f6c15cc 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" @@ -22793,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" @@ -28338,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"