diff --git a/.backportrc.json b/.backportrc.json index f9d0f001c35f6..05c3a60e3625a 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "targetBranchChoices": [ { "name": "master", "checked": true }, { "name": "7.x", "checked": true }, + "7.15", "7.14", "7.13", "7.12", @@ -32,7 +33,7 @@ "targetPRLabels": ["backport"], "branchLabelMapping": { "^v8.0.0$": "master", - "^v7.15.0$": "7.x", + "^v7.16.0$": "7.x", "^v(\\d+).(\\d+).\\d+$": "$1.$2" }, "autoMerge": true, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7ee84b6bc9e8d..b47c3b09cce30 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,6 +9,9 @@ /dev_docs @elastic/kibana-tech-leads /packages/kbn-docs-utils/ @elastic/kibana-tech-leads @elastic/kibana-operations +# Virtual teams +/x-pack/plugins/rule_registry/ @elastic/rac + # App /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app @@ -31,6 +34,7 @@ /src/plugins/vis_type_pie/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/visualizations/ @elastic/kibana-app +/src/plugins/url_forwarding/ @elastic/kibana-app /packages/kbn-tinymath/ @elastic/kibana-app # Application Services @@ -174,12 +178,8 @@ /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis /x-pack/plugins/stack_alerts/server/alert_types/geo_containment @elastic/kibana-gis /x-pack/plugins/stack_alerts/public/alert_types/geo_containment @elastic/kibana-gis -#CC# /src/plugins/maps_legacy/ @elastic/kibana-gis -/src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis /x-pack/plugins/file_upload @elastic/kibana-gis -/src/plugins/tile_map/ @elastic/kibana-gis -/src/plugins/region_map/ @elastic/kibana-gis /packages/kbn-mapbox-gl @elastic/kibana-gis # Operations @@ -369,6 +369,7 @@ /x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/security-solution /x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution /x-pack/plugins/security_solution/ @elastic/security-solution +/x-pack/plugins/metrics_entities/ @elastic/security-solution /x-pack/test/detection_engine_api_integration @elastic/security-solution /x-pack/test/lists_api_integration @elastic/security-solution /x-pack/test/api_integration/apis/security_solution @elastic/security-solution diff --git a/.i18nrc.json b/.i18nrc.json index 235b65d7502f4..d19226e6b6f8c 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -1,5 +1,6 @@ { "paths": { + "alerts": "packages/kbn-alerts/src", "autocomplete": "packages/kbn-securitysolution-autocomplete/src", "console": "src/plugins/console", "core": "src/core", @@ -32,7 +33,6 @@ "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", "lists": "packages/kbn-securitysolution-list-utils/src", "management": ["src/legacy/core_plugins/management", "src/plugins/management"], - "maps_legacy": "src/plugins/maps_legacy", "monaco": "packages/kbn-monaco/src", "esQuery": "packages/kbn-es-query/src", "presentationUtil": "src/plugins/presentation_util", @@ -48,14 +48,12 @@ "kibana_utils": "src/plugins/kibana_utils", "navigation": "src/plugins/navigation", "newsfeed": "src/plugins/newsfeed", - "regionMap": "src/plugins/region_map", "savedObjects": "src/plugins/saved_objects", "savedObjectsManagement": "src/plugins/saved_objects_management", "security": "src/plugins/security_oss", "server": "src/legacy/server", "statusPage": "src/legacy/core_plugins/status_page", "telemetry": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"], - "tileMap": "src/plugins/tile_map", "timelion": ["src/plugins/timelion", "src/plugins/vis_type_timelion"], "uiActions": "src/plugins/ui_actions", "visDefaultEditor": "src/plugins/vis_default_editor", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11c595a1ad983..14dfaa84cebb6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,5 @@ # Contributing to Kibana -We understand that you may not have days at a time to work on Kibana. We ask that you read our [developer guide](https://www.elastic.co/guide/en/kibana/master/development.html) carefully so that you spend less time, overall, struggling to push your PR through our code review processes. +If you are an employee at Elastic, please check out our Developer Guide [here](https://docs.elastic.dev/kibana-dev-docs/welcome). -Our developer guide is written in asciidoc and located under [./docs/developer](./docs/developer) if you want to make edits or access it in raw form. +If you are an external developer, we have a legacy developer guide [here](https://www.elastic.co/guide/en/kibana/master/development.html), or you can view the raw docs from our new, internal Developer Guide [here](./dev_docs/getting_started/dev_welcome.mdx). Eventually, our internal Developer Guide will be opened for public consumption. diff --git a/dev_docs/assets/kibana_template_no_data_config.png b/dev_docs/assets/kibana_template_no_data_config.png index 5e54bfdce1938..a3d12fc018503 100644 Binary files a/dev_docs/assets/kibana_template_no_data_config.png and b/dev_docs/assets/kibana_template_no_data_config.png differ diff --git a/dev_docs/tutorials/kibana_page_template.mdx b/dev_docs/tutorials/kibana_page_template.mdx index eab5b2eb3ce8e..bc0abc99d8921 100644 --- a/dev_docs/tutorials/kibana_page_template.mdx +++ b/dev_docs/tutorials/kibana_page_template.mdx @@ -126,7 +126,7 @@ This is a built-in configuration that displays a very specific UI and requires v The `noDataConfig` is of type [`NoDataPagProps`](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx): -1. `solution: string`: Single name for the current solution, used to auto-generate the title, logo, description, and button label *(required)* +1. `solution: string`: Single name for the current solution, used to auto-generate the title, logo, and description *(required)* 2. `docsLink: string`: Required to set the docs link for the whole solution *(required)* 3. `logo?: string`: Optionally replace the auto-generated logo 4. `pageTitle?: string`: Optionally replace the auto-generated page title (h1) @@ -136,7 +136,7 @@ The `noDataConfig` is of type [`NoDataPagProps`](https://github.com/elastic/kiba There are two main actions for adding data that we promote throughout Kibana, Elastic Agent and Beats. They are added to the cards that are displayed by using the keys `elasticAgent` and `beats` respectively. For consistent messaging, these two cards are pre-configured but require specific `href`s and/or `onClick` handlers for directing the user to the right location for that solution. -It also accepts a `recommended` prop as a boolean to promote one or more of the cards through visuals added to the UI. It will also place the `recommended` ones first in the list. By default, the configuration will recommend `elasticAgent`. Optionally you can also replace the `button` label by passing a string, or the whole component by passing a `ReactNode`. +It also accepts a `recommended` prop as a boolean to promote one or more of the cards through visuals added to the UI. It will also place the `recommended` ones first in the list. Optionally you can also replace the `button` label by passing a string, or the whole component by passing a `ReactNode`. ```tsx @@ -145,18 +145,20 @@ const hasData = checkForData(); // No data configuration const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = { - solution: 'Observability', + solution: 'Analytics', + logo: 'logoKibana', docsLink: '#', actions: { - elasticAgent: { + beats: { href: '#', }, - beats: { + elasticAgent: { href: '#', }, }, }; +// Conditionally apply the configuration if there is no data > and <>.] + Import and export dashboards with the corresponding saved objects, such as visualizations, saved searches, and index patterns. diff --git a/docs/api/dashboard/export-dashboard.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc index 6d239d755eb0d..3a20eff0a54d2 100644 --- a/docs/api/dashboard/export-dashboard.asciidoc +++ b/docs/api/dashboard/export-dashboard.asciidoc @@ -4,7 +4,9 @@ Export dashboard ++++ -experimental[] Export dashboards and corresponding saved objects. +deprecated::[7.15.0,Use <> instead.] + +Export dashboards and corresponding saved objects. [[dashboard-api-export-request]] ==== Request diff --git a/docs/api/dashboard/import-dashboard.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc index 5d1fab41a2a14..e4817d6cb7ee9 100644 --- a/docs/api/dashboard/import-dashboard.asciidoc +++ b/docs/api/dashboard/import-dashboard.asciidoc @@ -4,7 +4,9 @@ Import dashboard ++++ -experimental[] Import dashboards and corresponding saved objects. +deprecated::[7.15.0,Use <> instead.] + +Import dashboards and corresponding saved objects. [[dashboard-api-import-request]] ==== Request diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index dc410f2e5f2a5..6431d85ac1a51 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -182,10 +182,6 @@ management section itself. |Configuration of kibana-wide EMS settings and some higher level utilities. -|{kib-repo}blob/{branch}/src/plugins/maps_legacy/README.md[mapsLegacy] -|Internal objects used by the Coordinate, Region, and Vega visualizations. - - |{kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation] |The navigation plugins exports the TopNavMenu component. It also provides a stateful version of it on the start contract. @@ -200,10 +196,6 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s |The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). -|{kib-repo}blob/{branch}/src/plugins/region_map/README.md[regionMap] -|Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states. - - |{kib-repo}blob/{branch}/src/plugins/saved_objects/README.md[savedObjects] |NOTE: This plugin is deprecated and will be removed in 8.0. See https://github.com/elastic/kibana/issues/46435 for more information. @@ -247,10 +239,6 @@ generating deep links to other apps, and creating short URLs. |This plugin adds the Advanced Settings section for the Usage and Security Data collection (aka Telemetry). -|{kib-repo}blob/{branch}/src/plugins/tile_map/README.md[tileMap] -|Create a coordinate map. Display the results of a geohash_tile aggregation as bubbles, rectangles, or heatmap color blobs. - - |{kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion] |Contains the deprecated timelion application. For the timelion visualization, which also contains the timelion APIs and backend, look at the vis_type_timelion plugin. diff --git a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.indices.md b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.indices.md deleted file mode 100644 index e21c213b1c826..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.indices.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) > [indices](./kibana-plugin-core-server.assistanceapiresponse.indices.md) - -## AssistanceAPIResponse.indices property - -Signature: - -```typescript -indices: { - [indexName: string]: { - action_required: MIGRATION_ASSISTANCE_INDEX_ACTION; - }; - }; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md deleted file mode 100644 index 1daaf95a73d5d..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.assistanceapiresponse.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) - -## AssistanceAPIResponse interface - -> Warning: This API is now obsolete. -> -> 7.16 -> - -Signature: - -```typescript -export interface AssistanceAPIResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [indices](./kibana-plugin-core-server.assistanceapiresponse.indices.md) | {
[indexName: string]: {
action_required: MIGRATION_ASSISTANCE_INDEX_ACTION;
};
} | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md deleted file mode 100644 index 1031d733fed4a..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) - -## AssistantAPIClientParams interface - -> Warning: This API is now obsolete. -> -> 7.16 -> - -Signature: - -```typescript -export interface AssistantAPIClientParams extends GenericParams -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [method](./kibana-plugin-core-server.assistantapiclientparams.method.md) | 'GET' | | -| [path](./kibana-plugin-core-server.assistantapiclientparams.path.md) | '/_migration/assistance' | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.method.md b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.method.md deleted file mode 100644 index 1d93206fe5e14..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.method.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) > [method](./kibana-plugin-core-server.assistantapiclientparams.method.md) - -## AssistantAPIClientParams.method property - -Signature: - -```typescript -method: 'GET'; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.path.md b/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.path.md deleted file mode 100644 index 1386733d0d8a8..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.assistantapiclientparams.path.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) > [path](./kibana-plugin-core-server.assistantapiclientparams.path.md) - -## AssistantAPIClientParams.path property - -Signature: - -```typescript -path: '/_migration/assistance'; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md deleted file mode 100644 index fc1748d4db907..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationAPIClientParams](./kibana-plugin-core-server.deprecationapiclientparams.md) - -## DeprecationAPIClientParams interface - -> Warning: This API is now obsolete. -> -> 7.16 -> - -Signature: - -```typescript -export interface DeprecationAPIClientParams extends GenericParams -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [method](./kibana-plugin-core-server.deprecationapiclientparams.method.md) | 'GET' | | -| [path](./kibana-plugin-core-server.deprecationapiclientparams.path.md) | '/_migration/deprecations' | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.method.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.method.md deleted file mode 100644 index 71724c2467b87..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.method.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationAPIClientParams](./kibana-plugin-core-server.deprecationapiclientparams.md) > [method](./kibana-plugin-core-server.deprecationapiclientparams.method.md) - -## DeprecationAPIClientParams.method property - -Signature: - -```typescript -method: 'GET'; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.path.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.path.md deleted file mode 100644 index 3fe7b0e8e9237..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiclientparams.path.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationAPIClientParams](./kibana-plugin-core-server.deprecationapiclientparams.md) > [path](./kibana-plugin-core-server.deprecationapiclientparams.path.md) - -## DeprecationAPIClientParams.path property - -Signature: - -```typescript -path: '/_migration/deprecations'; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.cluster_settings.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.cluster_settings.md deleted file mode 100644 index ef612d0901682..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.cluster_settings.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) > [cluster\_settings](./kibana-plugin-core-server.deprecationapiresponse.cluster_settings.md) - -## DeprecationAPIResponse.cluster\_settings property - -Signature: - -```typescript -cluster_settings: DeprecationInfo[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.index_settings.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.index_settings.md deleted file mode 100644 index 50b2af591c5a7..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.index_settings.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) > [index\_settings](./kibana-plugin-core-server.deprecationapiresponse.index_settings.md) - -## DeprecationAPIResponse.index\_settings property - -Signature: - -```typescript -index_settings: IndexSettingsDeprecationInfo; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md deleted file mode 100644 index ce40bd7c750f0..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) - -## DeprecationAPIResponse interface - -> Warning: This API is now obsolete. -> -> 7.16 -> - -Signature: - -```typescript -export interface DeprecationAPIResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [cluster\_settings](./kibana-plugin-core-server.deprecationapiresponse.cluster_settings.md) | DeprecationInfo[] | | -| [index\_settings](./kibana-plugin-core-server.deprecationapiresponse.index_settings.md) | IndexSettingsDeprecationInfo | | -| [ml\_settings](./kibana-plugin-core-server.deprecationapiresponse.ml_settings.md) | DeprecationInfo[] | | -| [node\_settings](./kibana-plugin-core-server.deprecationapiresponse.node_settings.md) | DeprecationInfo[] | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.ml_settings.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.ml_settings.md deleted file mode 100644 index 641847fd1159d..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.ml_settings.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) > [ml\_settings](./kibana-plugin-core-server.deprecationapiresponse.ml_settings.md) - -## DeprecationAPIResponse.ml\_settings property - -Signature: - -```typescript -ml_settings: DeprecationInfo[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.node_settings.md b/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.node_settings.md deleted file mode 100644 index 9473fd2c1d1ad..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationapiresponse.node_settings.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) > [node\_settings](./kibana-plugin-core-server.deprecationapiresponse.node_settings.md) - -## DeprecationAPIResponse.node\_settings property - -Signature: - -```typescript -node_settings: DeprecationInfo[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.details.md b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.details.md deleted file mode 100644 index f33f2878a9d01..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.details.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationInfo](./kibana-plugin-core-server.deprecationinfo.md) > [details](./kibana-plugin-core-server.deprecationinfo.details.md) - -## DeprecationInfo.details property - -Signature: - -```typescript -details?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.level.md b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.level.md deleted file mode 100644 index 2543c19e141e1..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.level.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationInfo](./kibana-plugin-core-server.deprecationinfo.md) > [level](./kibana-plugin-core-server.deprecationinfo.level.md) - -## DeprecationInfo.level property - -Signature: - -```typescript -level: MIGRATION_DEPRECATION_LEVEL; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md deleted file mode 100644 index d9d1c6c3edb41..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationInfo](./kibana-plugin-core-server.deprecationinfo.md) - -## DeprecationInfo interface - -> Warning: This API is now obsolete. -> -> 7.16 -> - -Signature: - -```typescript -export interface DeprecationInfo -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [details](./kibana-plugin-core-server.deprecationinfo.details.md) | string | | -| [level](./kibana-plugin-core-server.deprecationinfo.level.md) | MIGRATION_DEPRECATION_LEVEL | | -| [message](./kibana-plugin-core-server.deprecationinfo.message.md) | string | | -| [url](./kibana-plugin-core-server.deprecationinfo.url.md) | string | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.message.md b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.message.md deleted file mode 100644 index 40bcc4f3a5b3d..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.message.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationInfo](./kibana-plugin-core-server.deprecationinfo.md) > [message](./kibana-plugin-core-server.deprecationinfo.message.md) - -## DeprecationInfo.message property - -Signature: - -```typescript -message: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.url.md b/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.url.md deleted file mode 100644 index 893d0bc10886c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationinfo.url.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationInfo](./kibana-plugin-core-server.deprecationinfo.md) > [url](./kibana-plugin-core-server.deprecationinfo.url.md) - -## DeprecationInfo.url property - -Signature: - -```typescript -url: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md index abcbbf18a8f9c..bcc2f474fa483 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md @@ -14,7 +14,5 @@ ```typescript legacy: { readonly config$: Observable; - readonly createClient: (type: string, clientConfig?: Partial) => ILegacyCustomClusterClient; - readonly client: ILegacyClusterClient; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md index ca6134cd5ed65..e6a4161674f5b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md @@ -15,5 +15,5 @@ export interface ElasticsearchServiceSetup | Property | Type | Description | | --- | --- | --- | -| [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) | {
readonly config$: Observable<ElasticsearchConfig>;
readonly createClient: (type: string, clientConfig?: Partial<LegacyElasticsearchClientConfig>) => ILegacyCustomClusterClient;
readonly client: ILegacyClusterClient;
} | | +| [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) | {
readonly config$: Observable<ElasticsearchConfig>;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md index 4026483894aa1..844ebf3815a99 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md @@ -14,7 +14,5 @@ ```typescript legacy: { readonly config$: Observable; - readonly createClient: (type: string, clientConfig?: Partial) => ILegacyCustomClusterClient; - readonly client: ILegacyClusterClient; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md index 8d9cd1be148cf..50216edb48f41 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md @@ -17,5 +17,5 @@ export interface ElasticsearchServiceStart | --- | --- | --- | | [client](./kibana-plugin-core-server.elasticsearchservicestart.client.md) | IClusterClient | A pre-configured [Elasticsearch client](./kibana-plugin-core-server.iclusterclient.md) | | [createClient](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). | -| [legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | {
readonly config$: Observable<ElasticsearchConfig>;
readonly createClient: (type: string, clientConfig?: Partial<LegacyElasticsearchClientConfig>) => ILegacyCustomClusterClient;
readonly client: ILegacyClusterClient;
} | | +| [legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | {
readonly config$: Observable<ElasticsearchConfig>;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md deleted file mode 100644 index d1e87feba0f03..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyclusterclient.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ILegacyClusterClient](./kibana-plugin-core-server.ilegacyclusterclient.md) - -## ILegacyClusterClient type - -> Warning: This API is now obsolete. -> -> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). 7.16 -> - -Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). - -See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). - -Signature: - -```typescript -export declare type ILegacyClusterClient = Pick; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md deleted file mode 100644 index c004ad2548802..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacycustomclusterclient.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ILegacyCustomClusterClient](./kibana-plugin-core-server.ilegacycustomclusterclient.md) - -## ILegacyCustomClusterClient type - -> Warning: This API is now obsolete. -> -> Use [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md). 7.16 -> - -Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). - -See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). - -Signature: - -```typescript -export declare type ILegacyCustomClusterClient = Pick; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md deleted file mode 100644 index 8e7ecdb9f7ec2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.ilegacyscopedclusterclient.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ILegacyScopedClusterClient](./kibana-plugin-core-server.ilegacyscopedclusterclient.md) - -## ILegacyScopedClusterClient type - -> Warning: This API is now obsolete. -> -> Use [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md). 7.16 -> - -Serves the same purpose as "normal" `ClusterClient` but exposes additional `callAsCurrentUser` method that doesn't use credentials of the Kibana internal user (as `callAsInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API. - -See [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md). - -Signature: - -```typescript -export declare type ILegacyScopedClusterClient = Pick; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md b/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md deleted file mode 100644 index 9103f9cfc6740..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.indexsettingsdeprecationinfo.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IndexSettingsDeprecationInfo](./kibana-plugin-core-server.indexsettingsdeprecationinfo.md) - -## IndexSettingsDeprecationInfo interface - -> Warning: This API is now obsolete. -> -> 7.16 -> - -Signature: - -```typescript -export interface IndexSettingsDeprecationInfo -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md b/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md deleted file mode 100644 index 2378e61484da5..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyapicaller.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md) - -## LegacyAPICaller interface - -> Warning: This API is now obsolete. -> -> 7.16 -> - -Signature: - -```typescript -export interface LegacyAPICaller -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md deleted file mode 100644 index 219180af26fd8..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) - -## LegacyCallAPIOptions interface - -> Warning: This API is now obsolete. -> -> 7.16 -> - -The set of options that defines how API call should be made and result be processed. - -Signature: - -```typescript -export interface LegacyCallAPIOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [signal](./kibana-plugin-core-server.legacycallapioptions.signal.md) | AbortSignal | A signal object that allows you to abort the request via an AbortController object. | -| [wrap401Errors](./kibana-plugin-core-server.legacycallapioptions.wrap401errors.md) | boolean | Indicates whether 401 Unauthorized errors returned from the Elasticsearch API should be wrapped into Boom error instances with properly set WWW-Authenticate header that could have been returned by the API itself. If API didn't specify that then Basic realm="Authorization Required" is used as WWW-Authenticate. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.signal.md b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.signal.md deleted file mode 100644 index 7d795a59e41a5..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.signal.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) > [signal](./kibana-plugin-core-server.legacycallapioptions.signal.md) - -## LegacyCallAPIOptions.signal property - -A signal object that allows you to abort the request via an AbortController object. - -Signature: - -```typescript -signal?: AbortSignal; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.wrap401errors.md b/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.wrap401errors.md deleted file mode 100644 index 38fac54db77a4..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacycallapioptions.wrap401errors.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) > [wrap401Errors](./kibana-plugin-core-server.legacycallapioptions.wrap401errors.md) - -## LegacyCallAPIOptions.wrap401Errors property - -Indicates whether `401 Unauthorized` errors returned from the Elasticsearch API should be wrapped into `Boom` error instances with properly set `WWW-Authenticate` header that could have been returned by the API itself. If API didn't specify that then `Basic realm="Authorization Required"` is used as `WWW-Authenticate`. - -Signature: - -```typescript -wrap401Errors?: boolean; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md deleted file mode 100644 index ed2763d980279..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) > [(constructor)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) - -## LegacyClusterClient.(constructor) - -Constructs a new instance of the `LegacyClusterClient` class - -Signature: - -```typescript -constructor(config: LegacyElasticsearchClientConfig, log: Logger, type: string, getAuthHeaders?: GetAuthHeaders); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| config | LegacyElasticsearchClientConfig | | -| log | Logger | | -| type | string | | -| getAuthHeaders | GetAuthHeaders | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.asscoped.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.asscoped.md deleted file mode 100644 index 1c25fc1d072b6..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.asscoped.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) > [asScoped](./kibana-plugin-core-server.legacyclusterclient.asscoped.md) - -## LegacyClusterClient.asScoped() method - -Creates an instance of [ILegacyScopedClusterClient](./kibana-plugin-core-server.ilegacyscopedclusterclient.md) based on the configuration the current cluster client that exposes additional `callAsCurrentUser` method scoped to the provided req. Consumers shouldn't worry about closing scoped client instances, these will be automatically closed as soon as the original cluster client isn't needed anymore and closed. - -Signature: - -```typescript -asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| request | ScopeableRequest | Request the IScopedClusterClient instance will be scoped to. Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform | - -Returns: - -`ILegacyScopedClusterClient` - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.callasinternaluser.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.callasinternaluser.md deleted file mode 100644 index 7c8cc18d24e29..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.callasinternaluser.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) > [callAsInternalUser](./kibana-plugin-core-server.legacyclusterclient.callasinternaluser.md) - -## LegacyClusterClient.callAsInternalUser property - -> Warning: This API is now obsolete. -> -> Use [IClusterClient.asInternalUser](./kibana-plugin-core-server.iclusterclient.asinternaluser.md). -> - -Calls specified endpoint with provided clientParams on behalf of the Kibana internal user. See [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md). - -Signature: - -```typescript -callAsInternalUser: LegacyAPICaller; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.close.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.close.md deleted file mode 100644 index 88a5ffce5bb17..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.close.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) > [close](./kibana-plugin-core-server.legacyclusterclient.close.md) - -## LegacyClusterClient.close() method - -Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. - -Signature: - -```typescript -close(): void; -``` -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md deleted file mode 100644 index 05855c31477c3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md +++ /dev/null @@ -1,38 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) - -## LegacyClusterClient class - -> Warning: This API is now obsolete. -> -> Use [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). 7.16 -> - -Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`). - -Signature: - -```typescript -export declare class LegacyClusterClient implements ILegacyClusterClient -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(config, log, type, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the LegacyClusterClient class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [callAsInternalUser](./kibana-plugin-core-server.legacyclusterclient.callasinternaluser.md) | | LegacyAPICaller | Calls specified endpoint with provided clientParams on behalf of the Kibana internal user. See [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md). | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [asScoped(request)](./kibana-plugin-core-server.legacyclusterclient.asscoped.md) | | Creates an instance of [ILegacyScopedClusterClient](./kibana-plugin-core-server.ilegacyscopedclusterclient.md) based on the configuration the current cluster client that exposes additional callAsCurrentUser method scoped to the provided req. Consumers shouldn't worry about closing scoped client instances, these will be automatically closed as soon as the original cluster client isn't needed anymore and closed. | -| [close()](./kibana-plugin-core-server.legacyclusterclient.close.md) | | Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md deleted file mode 100644 index a80ebe2fee493..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyElasticsearchClientConfig](./kibana-plugin-core-server.legacyelasticsearchclientconfig.md) - -## LegacyElasticsearchClientConfig type - -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -export declare type LegacyElasticsearchClientConfig = Pick & Pick & { - pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; - requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; - sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval']; - ssl?: Partial; -}; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror._code_.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror._code_.md deleted file mode 100644 index 05530ceb0d568..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror._code_.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) > [\[code\]](./kibana-plugin-core-server.legacyelasticsearcherror._code_.md) - -## LegacyElasticsearchError.\[code\] property - -Signature: - -```typescript -[code]?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md deleted file mode 100644 index 7cf696ad8d73f..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherror.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) - -## LegacyElasticsearchError interface - -@deprecated. The new elasticsearch client doesn't wrap errors anymore. 7.16 - -Signature: - -```typescript -export interface LegacyElasticsearchError extends Boom.Boom -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [\[code\]](./kibana-plugin-core-server.legacyelasticsearcherror._code_.md) | string | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherrorhelpers.decoratenotauthorizederror.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherrorhelpers.decoratenotauthorizederror.md deleted file mode 100644 index bd802a39e9339..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherrorhelpers.decoratenotauthorizederror.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyElasticsearchErrorHelpers](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md) > [decorateNotAuthorizedError](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.decoratenotauthorizederror.md) - -## LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError() method - -Signature: - -```typescript -static decorateNotAuthorizedError(error: Error, reason?: string): LegacyElasticsearchError; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| error | Error | | -| reason | string | | - -Returns: - -`LegacyElasticsearchError` - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherrorhelpers.isnotauthorizederror.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherrorhelpers.isnotauthorizederror.md deleted file mode 100644 index f647916149458..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherrorhelpers.isnotauthorizederror.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyElasticsearchErrorHelpers](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md) > [isNotAuthorizedError](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.isnotauthorizederror.md) - -## LegacyElasticsearchErrorHelpers.isNotAuthorizedError() method - -Signature: - -```typescript -static isNotAuthorizedError(error: any): error is LegacyElasticsearchError; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| error | any | | - -Returns: - -`error is LegacyElasticsearchError` - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md deleted file mode 100644 index e20dcd4ed253e..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md +++ /dev/null @@ -1,35 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyElasticsearchErrorHelpers](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md) - -## LegacyElasticsearchErrorHelpers class - -Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as `body.error.header[WWW-Authenticate]` - -Signature: - -```typescript -export declare class LegacyElasticsearchErrorHelpers -``` - -## Example - -Handle errors - -```js -try { - await client.asScoped(request).callAsCurrentUser(...); -} catch (err) { - if (ElasticsearchErrorHelpers.isNotAuthorizedError(err)) { - const authHeader = err.output.headers['WWW-Authenticate']; - } - -``` - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [decorateNotAuthorizedError(error, reason)](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.decoratenotauthorizederror.md) | static | | -| [isNotAuthorizedError(error)](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.isnotauthorizederror.md) | static | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md deleted file mode 100644 index bd1cd1e9f3d9b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) > [(constructor)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) - -## LegacyScopedClusterClient.(constructor) - -Constructs a new instance of the `LegacyScopedClusterClient` class - -Signature: - -```typescript -constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| internalAPICaller | LegacyAPICaller | | -| scopedAPICaller | LegacyAPICaller | | -| headers | Headers | undefined | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callascurrentuser.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callascurrentuser.md deleted file mode 100644 index 0f2d653e41a55..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callascurrentuser.md +++ /dev/null @@ -1,31 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) > [callAsCurrentUser](./kibana-plugin-core-server.legacyscopedclusterclient.callascurrentuser.md) - -## LegacyScopedClusterClient.callAsCurrentUser() method - -> Warning: This API is now obsolete. -> -> Use [IScopedClusterClient.asCurrentUser](./kibana-plugin-core-server.iscopedclusterclient.ascurrentuser.md). 7.16 -> - -Calls specified `endpoint` with provided `clientParams` on behalf of the user initiated request to the Kibana server (via HTTP request headers). See [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md). - -Signature: - -```typescript -callAsCurrentUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| endpoint | string | String descriptor of the endpoint e.g. cluster.getSettings or ping. | -| clientParams | Record<string, any> | A dictionary of parameters that will be passed directly to the Elasticsearch JS client. | -| options | LegacyCallAPIOptions | Options that affect the way we call the API and process the result. | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callasinternaluser.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callasinternaluser.md deleted file mode 100644 index 2c184b0fde5b3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.callasinternaluser.md +++ /dev/null @@ -1,31 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) > [callAsInternalUser](./kibana-plugin-core-server.legacyscopedclusterclient.callasinternaluser.md) - -## LegacyScopedClusterClient.callAsInternalUser() method - -> Warning: This API is now obsolete. -> -> Use [IScopedClusterClient.asInternalUser](./kibana-plugin-core-server.iscopedclusterclient.asinternaluser.md). 7.16 -> - -Calls specified `endpoint` with provided `clientParams` on behalf of the Kibana internal user. See [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md). - -Signature: - -```typescript -callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| endpoint | string | String descriptor of the endpoint e.g. cluster.getSettings or ping. | -| clientParams | Record<string, any> | A dictionary of parameters that will be passed directly to the Elasticsearch JS client. | -| options | LegacyCallAPIOptions | Options that affect the way we call the API and process the result. | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md deleted file mode 100644 index 6678c3bc16d53..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md +++ /dev/null @@ -1,32 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) - -## LegacyScopedClusterClient class - -> Warning: This API is now obsolete. -> -> Use [scoped cluster client](./kibana-plugin-core-server.iscopedclusterclient.md). 7.16 -> - -Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. - -Signature: - -```typescript -export declare class LegacyScopedClusterClient implements ILegacyScopedClusterClient -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(internalAPICaller, scopedAPICaller, headers)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) | | Constructs a new instance of the LegacyScopedClusterClient class | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [callAsCurrentUser(endpoint, clientParams, options)](./kibana-plugin-core-server.legacyscopedclusterclient.callascurrentuser.md) | | Calls specified endpoint with provided clientParams on behalf of the user initiated request to the Kibana server (via HTTP request headers). See [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md). | -| [callAsInternalUser(endpoint, clientParams, options)](./kibana-plugin-core-server.legacyscopedclusterclient.callasinternaluser.md) | | Calls specified endpoint with provided clientParams on behalf of the Kibana internal user. See [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md). | - diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 726432ae134dc..ba4f528352566 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -20,9 +20,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CspConfig](./kibana-plugin-core-server.cspconfig.md) | CSP configuration for use in Kibana. | | [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) | Wrapper of config schema. | | [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | -| [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | -| [LegacyElasticsearchErrorHelpers](./kibana-plugin-core-server.legacyelasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | -| [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [RouteValidationError](./kibana-plugin-core-server.routevalidationerror.md) | Error to return when the validation is not successful. | | [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | @@ -48,8 +45,6 @@ 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) | | | [AsyncPlugin](./kibana-plugin-core-server.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Authenticated](./kibana-plugin-core-server.authenticated.md) | | | [AuthNotHandled](./kibana-plugin-core-server.authnothandled.md) | | @@ -68,9 +63,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CountResponse](./kibana-plugin-core-server.countresponse.md) | | | [CustomHttpResponseOptions](./kibana-plugin-core-server.customhttpresponseoptions.md) | HTTP response parameters for a response with adjustable status code. | | [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) | | -| [DeprecationAPIClientParams](./kibana-plugin-core-server.deprecationapiclientparams.md) | | -| [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) | | -| [DeprecationInfo](./kibana-plugin-core-server.deprecationinfo.md) | | | [DeprecationsClient](./kibana-plugin-core-server.deprecationsclient.md) | Server-side client that provides access to fetch all Kibana deprecations | | [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) | | | [DeprecationSettings](./kibana-plugin-core-server.deprecationsettings.md) | UiSettings deprecation field options. | @@ -105,7 +97,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. | | [IKibanaResponse](./kibana-plugin-core-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution | | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | -| [IndexSettingsDeprecationInfo](./kibana-plugin-core-server.indexsettingsdeprecationinfo.md) | | | [IRenderOptions](./kibana-plugin-core-server.irenderoptions.md) | | | [IRouter](./kibana-plugin-core-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-core-server.routeconfig.md) and [RequestHandler](./kibana-plugin-core-server.requesthandler.md) for more information about arguments to route registrations. | | [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) | | @@ -113,9 +104,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | | [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. | | [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | -| [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md) | | -| [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | -| [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. 7.16 | | [LegacyRequest](./kibana-plugin-core-server.legacyrequest.md) | | | [LoggerContextConfigInput](./kibana-plugin-core-server.loggercontextconfiginput.md) | | | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. | @@ -139,7 +127,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PrebootPlugin](./kibana-plugin-core-server.prebootplugin.md) | The interface that should be returned by a PluginInitializer for a preboot plugin. | | [PrebootServicePreboot](./kibana-plugin-core-server.prebootservicepreboot.md) | Kibana Preboot Service allows to control the boot flow of Kibana. Preboot plugins can use it to hold the boot until certain condition is met. | | [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) | | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [ResolveCapabilitiesOptions](./kibana-plugin-core-server.resolvecapabilitiesoptions.md) | Defines a set of additional options for the resolveCapabilities method of [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md). | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | @@ -270,9 +258,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpResponsePayload](./kibana-plugin-core-server.httpresponsepayload.md) | Data send to the client as a response payload. | | [IBasePath](./kibana-plugin-core-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-core-server.basepath.md) | | [IContextProvider](./kibana-plugin-core-server.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | -| [ILegacyClusterClient](./kibana-plugin-core-server.ilegacyclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). | -| [ILegacyCustomClusterClient](./kibana-plugin-core-server.ilegacycustomclusterclient.md) | Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [LegacyClusterClient](./kibana-plugin-core-server.legacyclusterclient.md). | -| [ILegacyScopedClusterClient](./kibana-plugin-core-server.ilegacyscopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.See [LegacyScopedClusterClient](./kibana-plugin-core-server.legacyscopedclusterclient.md). | | [IsAuthenticated](./kibana-plugin-core-server.isauthenticated.md) | Returns authentication status for a request. | | [ISavedObjectsExporter](./kibana-plugin-core-server.isavedobjectsexporter.md) | | | [ISavedObjectsImporter](./kibana-plugin-core-server.isavedobjectsimporter.md) | | @@ -282,13 +267,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [KibanaRequestRouteOptions](./kibana-plugin-core-server.kibanarequestrouteoptions.md) | Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. | | [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. | | [KnownHeaders](./kibana-plugin-core-server.knownheaders.md) | Set of well-known HTTP headers. | -| [LegacyElasticsearchClientConfig](./kibana-plugin-core-server.legacyelasticsearchclientconfig.md) | | | [LifecycleResponseFactory](./kibana-plugin-core-server.lifecycleresponsefactory.md) | Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. | | [LoggerConfigType](./kibana-plugin-core-server.loggerconfigtype.md) | | | [MakeUsageFromSchema](./kibana-plugin-core-server.makeusagefromschema.md) | List of configuration values that will be exposed to usage collection. If parent node or actual config path is set to true then the actual value of these configs will be reoprted. If parent node or actual config path is set to false then the config will be reported as \[redacted\]. | | [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) | APIs to retrieves metrics gathered and exposed by the core platform. | -| [MIGRATION\_ASSISTANCE\_INDEX\_ACTION](./kibana-plugin-core-server.migration_assistance_index_action.md) | | -| [MIGRATION\_DEPRECATION\_LEVEL](./kibana-plugin-core-server.migration_deprecation_level.md) | | | [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | | [OnPostAuthHandler](./kibana-plugin-core-server.onpostauthhandler.md) | See [OnPostAuthToolkit](./kibana-plugin-core-server.onpostauthtoolkit.md). | | [OnPreAuthHandler](./kibana-plugin-core-server.onpreauthhandler.md) | See [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md). | diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md b/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md deleted file mode 100644 index ea0a277931eaf..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.migration_assistance_index_action.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MIGRATION\_ASSISTANCE\_INDEX\_ACTION](./kibana-plugin-core-server.migration_assistance_index_action.md) - -## MIGRATION\_ASSISTANCE\_INDEX\_ACTION type - -> Warning: This API is now obsolete. -> -> 7.16 -> - -Signature: - -```typescript -export declare type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md b/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md deleted file mode 100644 index f71e6e78a4c34..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.migration_deprecation_level.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MIGRATION\_DEPRECATION\_LEVEL](./kibana-plugin-core-server.migration_deprecation_level.md) - -## MIGRATION\_DEPRECATION\_LEVEL type - -> Warning: This API is now obsolete. -> -> 7.16 -> - -Signature: - -```typescript -export declare type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index e54e8f105a2bd..dcf6975c5fa70 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -17,9 +17,6 @@ core: { }; elasticsearch: { client: IScopedClusterClient; - legacy: { - client: ILegacyScopedClusterClient; - }; }; uiSettings: { client: IUiSettingsClient; diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 59069ec995493..b6d78f8890b37 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract;
getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter;
getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
deprecations: {
client: DeprecationsClient;
};
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
getClient: (options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract;
getExporter: (client: SavedObjectsClientContract) => ISavedObjectsExporter;
getImporter: (client: SavedObjectsClientContract) => ISavedObjectsImporter;
};
elasticsearch: {
client: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
deprecations: {
client: DeprecationsClient;
};
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md index e05f9466aa9ee..e17877a537d00 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md @@ -7,15 +7,15 @@ Signature: ```typescript -static createGenericNotFoundEsUnavailableError(type: string, id: string): DecoratedError; +static createGenericNotFoundEsUnavailableError(type?: string | null, id?: string | null): DecoratedError; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| type | string | | -| id | string | | +| type | string | null | | +| id | string | null | | Returns: 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 b5d5c5cfee5ad..2500ed9b2bc05 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 @@ -28,7 +28,7 @@ esFilters: { isPhraseFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").PhraseFilter; isExistsFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").ExistsFilter; isPhrasesFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").PhrasesFilter; - isRangeFilter: (filter?: import("@kbn/es-query").ExistsFilter | import("@kbn/es-query").GeoPolygonFilter | import("@kbn/es-query").PhrasesFilter | import("@kbn/es-query").PhraseFilter | import("@kbn/es-query").MatchAllFilter | import("@kbn/es-query").MissingFilter | import("@kbn/es-query").RangeFilter | import("@kbn/es-query").GeoBoundingBoxFilter | undefined) => filter is import("@kbn/es-query").RangeFilter; + isRangeFilter: (filter?: import("@kbn/es-query").ExistsFilter | import("@kbn/es-query").PhrasesFilter | import("@kbn/es-query").PhraseFilter | import("@kbn/es-query").MatchAllFilter | import("@kbn/es-query").MissingFilter | import("@kbn/es-query").RangeFilter | undefined) => filter is import("@kbn/es-query").RangeFilter; isMatchAllFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").MatchAllFilter; isMissingFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").MissingFilter; isQueryStringFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query/target_types/filters/build_filters").QueryStringFilter; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.id.md new file mode 100644 index 0000000000000..88c3a7d3654be --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) > [id](./kibana-plugin-plugins-data-public.indexpatternlistitem.id.md) + +## IndexPatternListItem.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.md new file mode 100644 index 0000000000000..609a5e0d9ef2c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) + +## IndexPatternListItem interface + +Signature: + +```typescript +export interface IndexPatternListItem +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-plugins-data-public.indexpatternlistitem.id.md) | string | | +| [title](./kibana-plugin-plugins-data-public.indexpatternlistitem.title.md) | string | | +| [type](./kibana-plugin-plugins-data-public.indexpatternlistitem.type.md) | string | | +| [typeMeta](./kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md) | TypeMeta | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.title.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.title.md new file mode 100644 index 0000000000000..26f292bf0d17b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) > [title](./kibana-plugin-plugins-data-public.indexpatternlistitem.title.md) + +## IndexPatternListItem.title property + +Signature: + +```typescript +title: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.type.md new file mode 100644 index 0000000000000..467e8bb81b159 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) > [type](./kibana-plugin-plugins-data-public.indexpatternlistitem.type.md) + +## IndexPatternListItem.type property + +Signature: + +```typescript +type?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md new file mode 100644 index 0000000000000..3b93c5111f8dd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) > [typeMeta](./kibana-plugin-plugins-data-public.indexpatternlistitem.typemeta.md) + +## IndexPatternListItem.typeMeta property + +Signature: + +```typescript +typeMeta?: TypeMeta; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md index ad2a167bd8c74..1f0148df596af 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md @@ -7,5 +7,5 @@ Signature: ```typescript -getCache: () => Promise[] | null | undefined>; +getCache: () => Promise>[] | null | undefined>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md index 7d29ced66afa8..b2dcddce0457c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md @@ -9,8 +9,5 @@ Get list of index pattern ids with titles Signature: ```typescript -getIdsWithTitle: (refresh?: boolean) => Promise>; +getIdsWithTitle: (refresh?: boolean) => Promise; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 26b393a5fb5b6..572a122066868 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -25,13 +25,13 @@ export declare class IndexPatternsService | [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | | [find](./kibana-plugin-plugins-data-public.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | Find and load index patterns by title | | [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | -| [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<Pick<IndexPatternAttributes, "type" | "title" | "typeMeta">>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | | [getDefaultId](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefaultid.md) | | () => Promise<string | null> | Get default index pattern id | | [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | | [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | -| [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>> | Get list of index pattern ids with titles | +| [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<IndexPatternListItem[]> | Get list of index pattern ids with titles | | [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | | [refreshFields](./kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | | [savedObjectToSpec](./kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md index e4ac35f19e959..93dfdeb056f15 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md @@ -7,5 +7,5 @@ Signature: ```typescript -isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean | undefined +isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 760f6d8651428..185dd771c4ace 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -66,6 +66,7 @@ | [IKibanaSearchRequest](./kibana-plugin-plugins-data-public.ikibanasearchrequest.md) | | | [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) | | | [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | Interface for an index pattern saved object | +| [IndexPatternListItem](./kibana-plugin-plugins-data-public.indexpatternlistitem.md) | | | [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) | Static index pattern format Serialized data object, representing index pattern attributes and state | | [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | @@ -84,6 +85,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | +| [TypeMeta](./kibana-plugin-plugins-data-public.typemeta.md) | | | [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) | Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | ## Variables diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index 5fffc5436e9c6..cd9bd61736225 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "isClearable" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "isClearable" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "displayStyle">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.aggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.aggs.md new file mode 100644 index 0000000000000..d2ab7ef72a4a5 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.aggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TypeMeta](./kibana-plugin-plugins-data-public.typemeta.md) > [aggs](./kibana-plugin-plugins-data-public.typemeta.aggs.md) + +## TypeMeta.aggs property + +Signature: + +```typescript +aggs?: Record; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.md new file mode 100644 index 0000000000000..dcc6500d54c5e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TypeMeta](./kibana-plugin-plugins-data-public.typemeta.md) + +## TypeMeta interface + +Signature: + +```typescript +export interface TypeMeta +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [aggs](./kibana-plugin-plugins-data-public.typemeta.aggs.md) | Record<string, AggregationRestrictions> | | +| [params](./kibana-plugin-plugins-data-public.typemeta.params.md) | {
rollup_index: string;
} | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.params.md new file mode 100644 index 0000000000000..6646f3c63ecc1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.typemeta.params.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TypeMeta](./kibana-plugin-plugins-data-public.typemeta.md) > [params](./kibana-plugin-plugins-data-public.typemeta.params.md) + +## TypeMeta.params property + +Signature: + +```typescript +params?: { + rollup_index: string; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md index 821c06984e55e..db765cf54d048 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md @@ -7,5 +7,5 @@ Signature: ```typescript -getCache: () => Promise[] | null | undefined>; +getCache: () => Promise>[] | null | undefined>; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md index 6433c78483545..a047b056e0ed5 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md @@ -9,8 +9,5 @@ Get list of index pattern ids with titles Signature: ```typescript -getIdsWithTitle: (refresh?: boolean) => Promise>; +getIdsWithTitle: (refresh?: boolean) => Promise; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index f5e845ced3cd1..64c46fe4abbd8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -25,13 +25,13 @@ export declare class IndexPatternsService | [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | | [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | Find and load index patterns by title | | [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | -| [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<Pick<IndexPatternAttributes, "type" | "title" | "typeMeta">>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | | [getDefaultId](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefaultid.md) | | () => Promise<string | null> | Get default index pattern id | | [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | | [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | -| [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>> | Get list of index pattern ids with titles | +| [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<IndexPatternListItem[]> | Get list of index pattern ids with titles | | [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | | [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | | [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index a31e4512cfcf1..5b884efe9909b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -10,7 +10,7 @@ start(core: CoreStart, { fieldFormats }: DataPluginStartDependencies): { fieldFormats: FieldFormatsStart; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -28,7 +28,7 @@ start(core: CoreStart, { fieldFormats }: DataPluginStartDependencies): { `{ fieldFormats: FieldFormatsStart; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md index b944c9dcc02a2..07ae46f8bbf12 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md @@ -18,6 +18,7 @@ export interface EmbeddableEditorState | --- | --- | --- | | [embeddableId](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.embeddableid.md) | string | | | [originatingApp](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingapp.md) | string | | +| [originatingPath](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md) | string | | | [searchSessionId](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.searchsessionid.md) | string | Pass current search session id when navigating to an editor, Editors could use it continue previous search session | | [valueInput](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.valueinput.md) | EmbeddableInput | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md new file mode 100644 index 0000000000000..e255f11f8a059 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [EmbeddableEditorState](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) > [originatingPath](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md) + +## EmbeddableEditorState.originatingPath property + +Signature: + +```typescript +originatingPath?: string; +``` diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 23f79c1bbb480..a5bdad16fa98f 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -534,17 +534,6 @@ of the chart. Use numbers between 0 and 1. The lower the number, the more the hi [[visualization-heatmap-maxbuckets]]`visualization:heatmap:maxBuckets`:: The maximum number of buckets a datasource can return. High numbers can have a negative impact on your browser rendering performance. -[[visualization-regionmap-showwarnings]]`visualization:regionmap:showWarnings`:: -Shows a warning in a region map when terms cannot be joined to a shape. - -[[visualization-tilemap-wmsdefaults]]`visualization:tileMap:WMSdefaults`:: -The default properties for the WMS map server supported in the coordinate map. - -[[visualization-tilemap-maxprecision]]`visualization:tileMap:maxPrecision`:: -The maximum geoHash precision displayed in tile maps. 7 is high, 10 is very high, -and 12 is the maximum. For more information, refer to -{ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[Cell dimensions at the equator]. - [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: **The legacy XY charts are deprecated and will not be supported as of 7.16.** The visualize editor uses a new XY charts library with improved performance, color palettes, fill capacity, and more. Enable this option if you prefer to use the legacy charts library. @@ -563,4 +552,4 @@ only production-ready visualizations are available to users. [horizontal] [[telemetry-enabled-advanced-setting]]`telemetry:enabled`:: When enabled, helps improve the Elastic Stack by providing usage statistics for -basic features. This data will not be shared outside of Elastic. +basic features. This data will not be shared outside of Elastic. \ No newline at end of file diff --git a/docs/management/connectors/action-types/servicenow.asciidoc b/docs/management/connectors/action-types/servicenow.asciidoc index 8655b126ae5db..3a4134cbf982e 100644 --- a/docs/management/connectors/action-types/servicenow.asciidoc +++ b/docs/management/connectors/action-types/servicenow.asciidoc @@ -18,6 +18,8 @@ URL:: ServiceNow instance URL. Username:: Username for HTTP Basic authentication. Password:: Password for HTTP Basic authentication. +The ServiceNow user requires at minimum read, create, and update access to the Incident table and read access to the https://docs.servicenow.com/bundle/paris-platform-administration/page/administer/localization/reference/r_ChoicesTable.html[sys_choice]. If you don't provide access to sys_choice, then the choices will not render. + [float] [[servicenow-connector-networking-configuration]] ==== Connector networking configuration diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 374dc4f735e9b..6430c5d246dc6 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -345,7 +345,7 @@ To share the dashboard with a larger audience, click *Share* in the toolbar. For [[import-dashboards]] == Export dashboards -To automate {kib}, you can export dashboards as JSON using the <>. It is important to export dashboards with all necessary references. +To automate {kib}, you can export dashboards as NDJSON using the <>. It is important to export dashboards with all necessary references. -- include::tutorial-create-a-dashboard-of-lens-panels.asciidoc[] diff --git a/examples/bfetch_explorer/kibana.json b/examples/bfetch_explorer/kibana.json index 4bd4492611812..0eda11670034c 100644 --- a/examples/bfetch_explorer/kibana.json +++ b/examples/bfetch_explorer/kibana.json @@ -4,6 +4,10 @@ "version": "0.0.1", "server": true, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["bfetch", "developerExamples"], "optionalPlugins": [], "requiredBundles": ["kibanaReact"] diff --git a/examples/developer_examples/kibana.json b/examples/developer_examples/kibana.json index 9e6b54c7af67c..a744b53137dc7 100644 --- a/examples/developer_examples/kibana.json +++ b/examples/developer_examples/kibana.json @@ -1,5 +1,9 @@ { "id": "developerExamples", + "owner": { + "name": "Kibana Core", + "githubTeam": "kibana-core" + }, "kibanaVersion": "kibana", "version": "0.0.1", "ui": true diff --git a/examples/expressions_explorer/kibana.json b/examples/expressions_explorer/kibana.json index 7e2062ff0a588..770ce91143d99 100644 --- a/examples/expressions_explorer/kibana.json +++ b/examples/expressions_explorer/kibana.json @@ -4,6 +4,10 @@ "version": "0.0.1", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["expressions", "inspector", "uiActions", "developerExamples"], "optionalPlugins": [], "requiredBundles": [] diff --git a/package.json b/package.json index 00fa0807e0f93..77836c8e29c98 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "dependencies": { "@elastic/apm-rum": "^5.8.0", "@elastic/apm-rum-react": "^1.2.11", - "@elastic/charts": "33.2.2", + "@elastic/charts": "34.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.17", "@elastic/ems-client": "7.15.0", @@ -121,6 +121,7 @@ "@hapi/podium": "^4.1.1", "@hapi/wreck": "^17.1.0", "@kbn/ace": "link:bazel-bin/packages/kbn-ace", + "@kbn/alerts": "link:bazel-bin/packages/kbn-alerts", "@kbn/analytics": "link:bazel-bin/packages/kbn-analytics", "@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader", "@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils", @@ -227,7 +228,6 @@ "deepmerge": "^4.2.2", "del": "^5.1.0", "elastic-apm-node": "^3.16.0", - "elasticsearch": "^16.7.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", "expiry-js": "0.1.7", @@ -280,10 +280,6 @@ "jsonwebtoken": "^8.5.1", "jsts": "^1.6.2", "kea": "^2.4.2", - "leaflet": "1.5.1", - "leaflet-draw": "0.4.14", - "leaflet-responsive-popup": "0.6.4", - "leaflet.heat": "0.2.0", "less": "npm:@elastic/less@2.7.3-kibana", "load-json-file": "^6.2.0", "loader-utils": "^1.2.3", @@ -379,6 +375,7 @@ "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", "redux-thunks": "^1.0.0", + "remark-stringify": "^9.0.0", "regenerator-runtime": "^0.13.3", "request": "^2.88.0", "require-in-the-middle": "^5.0.2", @@ -739,10 +736,10 @@ "gulp": "4.0.2", "gulp-babel": "^8.0.0", "gulp-brotli": "^3.0.0", + "gulp-gzip": "^1.4.2", "gulp-postcss": "^8.0.0", "gulp-sourcemaps": "2.6.5", "gulp-terser": "^2.0.1", - "gulp-gzip": "^1.4.2", "gulp-zip": "^5.0.2", "has-ansi": "^3.0.0", "hdr-histogram-js": "^1.2.0", @@ -828,8 +825,8 @@ "tar-fs": "^2.1.0", "tempy": "^0.3.0", "terminal-link": "^2.1.1", - "terser-webpack-plugin": "^2.1.2", "terser": "^5.7.1", + "terser-webpack-plugin": "^2.1.2", "ts-loader": "^7.0.5", "ts-morph": "^9.1.0", "tsd": "^0.13.1", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 5a8aa75ee255e..5c29b4a7eb64b 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -7,6 +7,7 @@ filegroup( "//packages/elastic-eslint-config-kibana:build", "//packages/elastic-safer-lodash-set:build", "//packages/kbn-ace:build", + "//packages/kbn-alerts:build", "//packages/kbn-analytics:build", "//packages/kbn-apm-config-loader:build", "//packages/kbn-apm-utils:build", diff --git a/packages/kbn-alerts/BUILD.bazel b/packages/kbn-alerts/BUILD.bazel new file mode 100644 index 0000000000000..c585b4430bfcb --- /dev/null +++ b/packages/kbn-alerts/BUILD.bazel @@ -0,0 +1,123 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-alerts" + +PKG_REQUIRE_NAME = "@kbn/alerts" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx" + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*", + "**/*.mocks.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "react/package.json", + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-babel-preset", + "//packages/kbn-dev-utils", + "//packages/kbn-i18n", + "@npm//@babel/core", + "@npm//babel-loader", + "@npm//@elastic/eui", + "@npm//react", + "@npm//resize-observer-polyfill", + "@npm//rxjs", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//typescript", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", + "//:tsconfig.browser_bazel.json", + ], +) + +ts_project( + name = "tsc", + args = ["--pretty"], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = True, + declaration_dir = "target_types", + declaration_map = True, + out_dir = "target_node", + root_dir = "src", + source_map = True, + tsconfig = ":tsconfig", +) + +ts_project( + name = "tsc_browser", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = False, + out_dir = "target_web", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig_browser", +) + +js_library( + name = PKG_BASE_NAME, + package_name = PKG_REQUIRE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + visibility = ["//visibility:public"], + deps = [":tsc", ":tsc_browser"] + DEPS, +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/packages/kbn-alerts/README.md b/packages/kbn-alerts/README.md new file mode 100644 index 0000000000000..6c545d2e44dfb --- /dev/null +++ b/packages/kbn-alerts/README.md @@ -0,0 +1,7 @@ +# AlertsFeatureNoPermissions + +Component displayed when a user with alerts permissions of `none` attempts to access alerts page. + +## useGetUserAlertsPermissions + +This hook parses through the uiCapabilities Kibana object to determine if the user has Kibana `read` or `crud` permissions for alerts. diff --git a/src/plugins/region_map/jest.config.js b/packages/kbn-alerts/babel.config.js similarity index 65% rename from src/plugins/region_map/jest.config.js rename to packages/kbn-alerts/babel.config.js index 1107c994c0443..b4a118df51af5 100644 --- a/src/plugins/region_map/jest.config.js +++ b/packages/kbn-alerts/babel.config.js @@ -7,7 +7,13 @@ */ module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/region_map'], + env: { + web: { + presets: ['@kbn/babel-preset/webpack_preset'], + }, + node: { + presets: ['@kbn/babel-preset/node_preset'], + }, + }, + ignore: ['**/*.test.ts', '**/*.test.tsx'], }; diff --git a/src/plugins/tile_map/jest.config.js b/packages/kbn-alerts/jest.config.js similarity index 85% rename from src/plugins/tile_map/jest.config.js rename to packages/kbn-alerts/jest.config.js index 7c51c61eca2a4..6ef365679ef74 100644 --- a/src/plugins/tile_map/jest.config.js +++ b/packages/kbn-alerts/jest.config.js @@ -8,6 +8,6 @@ module.exports = { preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/tile_map'], + rootDir: '../..', + roots: ['/packages/kbn-alerts'], }; diff --git a/packages/kbn-alerts/package.json b/packages/kbn-alerts/package.json new file mode 100644 index 0000000000000..b52a6efc35139 --- /dev/null +++ b/packages/kbn-alerts/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/alerts", + "version": "1.0.0", + "description": "Alerts components and hooks", + "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", + "private": true +} diff --git a/packages/kbn-alerts/react/package.json b/packages/kbn-alerts/react/package.json new file mode 100644 index 0000000000000..c5f222b5843ac --- /dev/null +++ b/packages/kbn-alerts/react/package.json @@ -0,0 +1,5 @@ +{ + "browser": "../target_web/react", + "main": "../target_node/react", + "types": "../target_types/react/index.d.ts" +} diff --git a/packages/kbn-alerts/src/empty_page/__snapshots__/index.test.tsx.snap b/packages/kbn-alerts/src/empty_page/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..59c877a20b8b5 --- /dev/null +++ b/packages/kbn-alerts/src/empty_page/__snapshots__/index.test.tsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyPage component renders actions with descriptions 1`] = ` + + + + Do Something + + } + title={false} + /> + + + } + title={ +

+ My Super Title +

+ } +/> +`; + +exports[`EmptyPage component renders actions without descriptions 1`] = ` + + + + Do Something + + + + } + title={ +

+ My Super Title +

+ } +/> +`; diff --git a/packages/kbn-alerts/src/empty_page/index.test.tsx b/packages/kbn-alerts/src/empty_page/index.test.tsx new file mode 100644 index 0000000000000..be755b0c23c38 --- /dev/null +++ b/packages/kbn-alerts/src/empty_page/index.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { EmptyPage } from './'; + +describe('EmptyPage component', () => { + it('renders actions without descriptions', () => { + const actions = { + actions: { + label: 'Do Something', + url: 'my/url/from/nowwhere', + }, + }; + const EmptyComponent = shallow(); + expect(EmptyComponent).toMatchSnapshot(); + }); + + it('renders actions with descriptions', () => { + const actions = { + actions: { + description: 'My Description', + label: 'Do Something', + url: 'my/url/from/nowwhere', + }, + }; + const EmptyComponent = shallow(); + expect(EmptyComponent).toMatchSnapshot(); + }); +}); diff --git a/packages/kbn-alerts/src/empty_page/index.tsx b/packages/kbn-alerts/src/empty_page/index.tsx new file mode 100644 index 0000000000000..186f4c898309b --- /dev/null +++ b/packages/kbn-alerts/src/empty_page/index.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiButton, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + IconType, + EuiCard, +} from '@elastic/eui'; +import React, { MouseEventHandler, ReactNode, useMemo } from 'react'; + +interface EmptyPageActions { + icon?: IconType; + label: string; + target?: string; + url: string; + descriptionTitle?: string; + description?: string; + fill?: boolean; + onClick?: MouseEventHandler; +} + +export type EmptyPageActionsProps = Record; + +interface EmptyPageProps { + actions: EmptyPageActionsProps; + 'data-test-subj'?: string; + message?: ReactNode; + title: string; + iconType?: IconType; +} + +const EmptyPageComponent = React.memo( + ({ actions, message, title, iconType, ...rest }) => { + const titles = Object.keys(actions); + const maxItemWidth = 283; + const renderActions = useMemo( + () => + Object.values(actions) + .filter((a) => a.label && a.url) + .map( + ( + { icon, label, target, url, descriptionTitle, description, onClick, fill = true }, + idx + ) => + descriptionTitle != null || description != null ? ( + + + {label} + + } + /> + + ) : ( + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + {label} + + + ) + ), + [actions, titles] + ); + + return ( + {title}} + body={message &&

{message}

} + actions={{renderActions}} + {...rest} + /> + ); + } +); + +EmptyPageComponent.displayName = 'EmptyPageComponent'; + +export const EmptyPage = React.memo(EmptyPageComponent); +EmptyPage.displayName = 'EmptyPage'; diff --git a/packages/kbn-alerts/src/features_no_permissions/index.tsx b/packages/kbn-alerts/src/features_no_permissions/index.tsx new file mode 100644 index 0000000000000..6dabf06e87c47 --- /dev/null +++ b/packages/kbn-alerts/src/features_no_permissions/index.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IconType } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { EmptyPage } from '../empty_page'; +import * as i18n from '../translations'; + +interface AlertsFeatureNoPermissionsProps { + documentationUrl: string; + iconType: IconType; +} + +export const AlertsFeatureNoPermissions: React.FC = ({ + documentationUrl, + iconType, +}): JSX.Element => { + const actions = useMemo( + () => ({ + feature: { + icon: 'documents', + label: i18n.GO_TO_DOCUMENTATION, + url: documentationUrl, + target: '_blank', + }, + }), + [documentationUrl] + ); + + return ( + + ); +}; + +AlertsFeatureNoPermissions.displayName = 'AlertsFeatureNoPermissions'; diff --git a/src/plugins/tile_map/server/index.ts b/packages/kbn-alerts/src/hooks/index.ts similarity index 84% rename from src/plugins/tile_map/server/index.ts rename to packages/kbn-alerts/src/hooks/index.ts index a7fa77a67d909..aed5e97992686 100644 --- a/src/plugins/tile_map/server/index.ts +++ b/packages/kbn-alerts/src/hooks/index.ts @@ -5,8 +5,4 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -export const plugin = () => ({ - setup() {}, - start() {}, -}); +export * from './use_get_alerts_permissions'; diff --git a/packages/kbn-alerts/src/hooks/use_get_alerts_permissions/index.ts b/packages/kbn-alerts/src/hooks/use_get_alerts_permissions/index.ts new file mode 100644 index 0000000000000..2f2c0967c32f2 --- /dev/null +++ b/packages/kbn-alerts/src/hooks/use_get_alerts_permissions/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useEffect, useState } from 'react'; + +// TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 +// import { Capabilities } from 'kibana/public'; +type Capabilities = any; +export interface UseGetUserAlertsPermissionsProps { + crud: boolean; + read: boolean; + loading: boolean; +} + +export const useGetUserAlertsPermissions = ( + uiCapabilities: Capabilities, + featureId: string +): UseGetUserAlertsPermissionsProps => { + const [alertsPermissions, setAlertsPermissions] = useState({ + crud: false, + read: false, + loading: true, + }); + + useEffect(() => { + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities[featureId].crud_alerts === 'boolean' + ? uiCapabilities[featureId].crud_alerts + : false; + const capabilitiesCanUserRead: boolean = + typeof uiCapabilities[featureId].read_alerts === 'boolean' + ? uiCapabilities[featureId].read_alerts + : false; + setAlertsPermissions({ + crud: capabilitiesCanUserCRUD, + read: capabilitiesCanUserRead, + loading: false, + }); + }, [featureId, uiCapabilities]); + + return alertsPermissions; +}; diff --git a/src/plugins/maps_legacy/public/tooltip_provider.d.ts b/packages/kbn-alerts/src/index.ts similarity index 79% rename from src/plugins/maps_legacy/public/tooltip_provider.d.ts rename to packages/kbn-alerts/src/index.ts index 33a17cc472d6c..304516f54ba17 100644 --- a/src/plugins/maps_legacy/public/tooltip_provider.d.ts +++ b/packages/kbn-alerts/src/index.ts @@ -5,5 +5,5 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -export function mapTooltipProvider(element: unknown, formatter: unknown): () => unknown; +export * from './hooks'; +export * from './features_no_permissions'; diff --git a/packages/kbn-alerts/src/translations/index.ts b/packages/kbn-alerts/src/translations/index.ts new file mode 100644 index 0000000000000..2d540ba1452d9 --- /dev/null +++ b/packages/kbn-alerts/src/translations/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const FEATURE_NO_PERMISSIONS_TITLE = i18n.translate('alerts.noPermissionsTitle', { + defaultMessage: 'Kibana feature privileges required', +}); + +export const ALERTS_FEATURE_NO_PERMISSIONS_MSG = i18n.translate('alerts.noPermissionsMessage', { + defaultMessage: + 'To view alerts, you must have privileges for the Alerts feature in the Kibana space. For more information, contact your Kibana administrator.', +}); + +export const GO_TO_DOCUMENTATION = i18n.translate('alerts.documentationTitle', { + defaultMessage: 'View documentation', +}); diff --git a/packages/kbn-alerts/tsconfig.browser.json b/packages/kbn-alerts/tsconfig.browser.json new file mode 100644 index 0000000000000..bb58f529eb0bb --- /dev/null +++ b/packages/kbn-alerts/tsconfig.browser.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.browser_bazel.json", + "compilerOptions": { + "allowJs": true, + "outDir": "./target_web", + "declaration": false, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-alerts/src", + "types": [ + "jest", + "node" + ], + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} \ No newline at end of file diff --git a/packages/kbn-alerts/tsconfig.json b/packages/kbn-alerts/tsconfig.json new file mode 100644 index 0000000000000..6a791ca2e5844 --- /dev/null +++ b/packages/kbn-alerts/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "allowJs": true, + "declarationDir": "./target_types", + "outDir": "target_node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-alerts/src", + "rootDir": "src", + "types": ["jest", "node", "resize-observer-polyfill"] + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/packages/kbn-es-query/src/filters/build_filters/geo_bounding_box_filter.test.ts b/packages/kbn-es-query/src/filters/build_filters/geo_bounding_box_filter.test.ts deleted file mode 100644 index af42e3f2c73fb..0000000000000 --- a/packages/kbn-es-query/src/filters/build_filters/geo_bounding_box_filter.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getGeoBoundingBoxFilterField } from './geo_bounding_box_filter'; - -describe('geo_bounding_box filter', function () { - describe('getGeoBoundingBoxFilterField', function () { - it('should return the name of the field a geo_bounding_box query is targeting', () => { - const filter = { - geo_bounding_box: { - geoPointField: { - bottom_right: { lat: 1, lon: 1 }, - top_left: { lat: 1, lon: 1 }, - }, - ignore_unmapped: true, - }, - meta: { - disabled: false, - negate: false, - alias: null, - params: { - bottom_right: { lat: 1, lon: 1 }, - top_left: { lat: 1, lon: 1 }, - }, - }, - }; - const result = getGeoBoundingBoxFilterField(filter); - expect(result).toBe('geoPointField'); - }); - }); -}); diff --git a/packages/kbn-es-query/src/filters/build_filters/geo_bounding_box_filter.ts b/packages/kbn-es-query/src/filters/build_filters/geo_bounding_box_filter.ts deleted file mode 100644 index 9066b695c17fc..0000000000000 --- a/packages/kbn-es-query/src/filters/build_filters/geo_bounding_box_filter.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { has } from 'lodash'; -import type { FieldFilter, Filter, FilterMeta, LatLon } from './types'; - -export type GeoBoundingBoxFilterMeta = FilterMeta & { - params: { - bottom_right: LatLon; - top_left: LatLon; - }; -}; - -export type GeoBoundingBoxFilter = Filter & { - meta: GeoBoundingBoxFilterMeta; - geo_bounding_box: any; -}; - -/** - * @param filter - * @returns `true` if a filter is an `GeoBoundingBoxFilter` - * - * @public - */ -export const isGeoBoundingBoxFilter = (filter: FieldFilter): filter is GeoBoundingBoxFilter => - has(filter, 'geo_bounding_box'); - -/** - * @internal - */ -export const getGeoBoundingBoxFilterField = (filter: GeoBoundingBoxFilter) => { - return ( - filter.geo_bounding_box && - Object.keys(filter.geo_bounding_box).find((key) => key !== 'ignore_unmapped') - ); -}; diff --git a/packages/kbn-es-query/src/filters/build_filters/geo_polygon_filter.test.ts b/packages/kbn-es-query/src/filters/build_filters/geo_polygon_filter.test.ts deleted file mode 100644 index 919ccbe427cd6..0000000000000 --- a/packages/kbn-es-query/src/filters/build_filters/geo_polygon_filter.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getGeoPolygonFilterField } from './geo_polygon_filter'; - -describe('geo_polygon filter', function () { - describe('getGeoPolygonFilterField', function () { - it('should return the name of the field a geo_polygon query is targeting', () => { - const filter = { - geo_polygon: { - geoPointField: { - points: [{ lat: 1, lon: 1 }], - }, - ignore_unmapped: true, - }, - meta: { - disabled: false, - negate: false, - alias: null, - params: { - points: [{ lat: 1, lon: 1 }], - }, - }, - }; - const result = getGeoPolygonFilterField(filter); - expect(result).toBe('geoPointField'); - }); - }); -}); diff --git a/packages/kbn-es-query/src/filters/build_filters/geo_polygon_filter.ts b/packages/kbn-es-query/src/filters/build_filters/geo_polygon_filter.ts deleted file mode 100644 index edeccdcf28b26..0000000000000 --- a/packages/kbn-es-query/src/filters/build_filters/geo_polygon_filter.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { has } from 'lodash'; -import type { FieldFilter, Filter, FilterMeta, LatLon } from './types'; - -export type GeoPolygonFilterMeta = FilterMeta & { - params: { - points: LatLon[]; - }; -}; - -export type GeoPolygonFilter = Filter & { - meta: GeoPolygonFilterMeta; - geo_polygon: any; -}; - -/** - * @param filter - * @returns `true` if a filter is an `GeoPolygonFilter` - * - * @public - */ -export const isGeoPolygonFilter = (filter: FieldFilter): filter is GeoPolygonFilter => - has(filter, 'geo_polygon'); - -/** - * @internal - */ -export const getGeoPolygonFilterField = (filter: GeoPolygonFilter) => { - return ( - filter.geo_polygon && Object.keys(filter.geo_polygon).find((key) => key !== 'ignore_unmapped') - ); -}; diff --git a/packages/kbn-es-query/src/filters/build_filters/get_filter_field.ts b/packages/kbn-es-query/src/filters/build_filters/get_filter_field.ts index 4ebed12e1237e..70949be18a61f 100644 --- a/packages/kbn-es-query/src/filters/build_filters/get_filter_field.ts +++ b/packages/kbn-es-query/src/filters/build_filters/get_filter_field.ts @@ -7,8 +7,6 @@ */ import { getExistsFilterField, isExistsFilter } from './exists_filter'; -import { getGeoBoundingBoxFilterField, isGeoBoundingBoxFilter } from './geo_bounding_box_filter'; -import { getGeoPolygonFilterField, isGeoPolygonFilter } from './geo_polygon_filter'; import { getMissingFilterField, isMissingFilter } from './missing_filter'; import { getPhrasesFilterField, isPhrasesFilter } from './phrases_filter'; import { getPhraseFilterField, isPhraseFilter } from './phrase_filter'; @@ -20,12 +18,6 @@ export const getFilterField = (filter: Filter) => { if (isExistsFilter(filter)) { return getExistsFilterField(filter); } - if (isGeoBoundingBoxFilter(filter)) { - return getGeoBoundingBoxFilterField(filter); - } - if (isGeoPolygonFilter(filter)) { - return getGeoPolygonFilterField(filter); - } if (isPhraseFilter(filter)) { return getPhraseFilterField(filter); } diff --git a/packages/kbn-es-query/src/filters/build_filters/index.ts b/packages/kbn-es-query/src/filters/build_filters/index.ts index c8b96d8b8efc9..7f81d83e6627d 100644 --- a/packages/kbn-es-query/src/filters/build_filters/index.ts +++ b/packages/kbn-es-query/src/filters/build_filters/index.ts @@ -11,8 +11,6 @@ export * from './build_filters'; export * from './build_empty_filter'; export * from './custom_filter'; export * from './exists_filter'; -export * from './geo_bounding_box_filter'; -export * from './geo_polygon_filter'; export * from './get_filter_field'; export * from './get_filter_params'; export * from './match_all_filter'; diff --git a/packages/kbn-es-query/src/filters/build_filters/types.ts b/packages/kbn-es-query/src/filters/build_filters/types.ts index 4bf53303c1544..5aad7d8735f8c 100644 --- a/packages/kbn-es-query/src/filters/build_filters/types.ts +++ b/packages/kbn-es-query/src/filters/build_filters/types.ts @@ -7,8 +7,6 @@ */ import { ExistsFilter } from './exists_filter'; -import { GeoBoundingBoxFilter } from './geo_bounding_box_filter'; -import { GeoPolygonFilter } from './geo_polygon_filter'; import { PhrasesFilter } from './phrases_filter'; import { PhraseFilter } from './phrase_filter'; import { RangeFilter } from './range_filter'; @@ -21,8 +19,6 @@ import { MissingFilter } from './missing_filter'; **/ export type FieldFilter = | ExistsFilter - | GeoBoundingBoxFilter - | GeoPolygonFilter | PhraseFilter | PhrasesFilter | RangeFilter @@ -49,8 +45,6 @@ export enum FILTERS { QUERY_STRING = 'query_string', RANGE = 'range', RANGE_FROM_VALUE = 'range_from_value', - GEO_BOUNDING_BOX = 'geo_bounding_box', - GEO_POLYGON = 'geo_polygon', SPATIAL_FILTER = 'spatial_filter', } diff --git a/packages/kbn-es-query/src/filters/index.ts b/packages/kbn-es-query/src/filters/index.ts index 61ee6ce6f0da6..6e82c21b6a529 100644 --- a/packages/kbn-es-query/src/filters/index.ts +++ b/packages/kbn-es-query/src/filters/index.ts @@ -30,8 +30,6 @@ export { export { isExistsFilter, isMatchAllFilter, - isGeoBoundingBoxFilter, - isGeoPolygonFilter, isMissingFilter, isPhraseFilter, isPhrasesFilter, @@ -67,11 +65,9 @@ export { PhraseFilter, PhrasesFilter, RangeFilterMeta, - GeoPolygonFilter, MatchAllFilter, CustomFilter, MissingFilter, - GeoBoundingBoxFilter, RangeFilterParams, } from './build_filters'; diff --git a/packages/kbn-legacy-logging/.babelrc b/packages/kbn-legacy-logging/.babelrc new file mode 100644 index 0000000000000..7da72d1779128 --- /dev/null +++ b/packages/kbn-legacy-logging/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-legacy-logging/BUILD.bazel b/packages/kbn-legacy-logging/BUILD.bazel index 7a9b472ca9553..1148cf1d38b65 100644 --- a/packages/kbn-legacy-logging/BUILD.bazel +++ b/packages/kbn-legacy-logging/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-legacy-logging" PKG_REQUIRE_NAME = "@kbn/legacy-logging" @@ -23,7 +24,7 @@ NPM_MODULE_EXTRA_FILES = [ "README.md" ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-config-schema", "//packages/kbn-utils", "@npm//@elastic/numeral", @@ -37,6 +38,13 @@ SRC_DEPS = [ ] TYPES_DEPS = [ + "//packages/kbn-config-schema", + "//packages/kbn-utils", + "@npm//@elastic/numeral", + "@npm//chokidar", + "@npm//query-string", + "@npm//rxjs", + "@npm//tslib", "@npm//@types/hapi__hapi", "@npm//@types/hapi__podium", "@npm//@types/jest", @@ -45,7 +53,11 @@ TYPES_DEPS = [ "@npm//@types/node", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -57,13 +69,14 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - out_dir = "target", + emit_declaration_only = True, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -72,7 +85,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-legacy-logging/package.json b/packages/kbn-legacy-logging/package.json index 77fcbb9904919..6e846ffc5bfaf 100644 --- a/packages/kbn-legacy-logging/package.json +++ b/packages/kbn-legacy-logging/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts" + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts" } diff --git a/packages/kbn-legacy-logging/tsconfig.json b/packages/kbn-legacy-logging/tsconfig.json index 30a2e56602b6f..55047dbcadc91 100644 --- a/packages/kbn-legacy-logging/tsconfig.json +++ b/packages/kbn-legacy-logging/tsconfig.json @@ -1,13 +1,14 @@ { "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "outDir": "target", - "stripInternal": false, "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-legacy-logging/src", + "stripInternal": false, "types": ["jest", "node"] }, "include": ["src/**/*"] diff --git a/packages/kbn-mapbox-gl/.babelrc b/packages/kbn-mapbox-gl/.babelrc new file mode 100644 index 0000000000000..7da72d1779128 --- /dev/null +++ b/packages/kbn-mapbox-gl/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-mapbox-gl/BUILD.bazel b/packages/kbn-mapbox-gl/BUILD.bazel index 3cbf7c39421e2..e0de6848c2289 100644 --- a/packages/kbn-mapbox-gl/BUILD.bazel +++ b/packages/kbn-mapbox-gl/BUILD.bazel @@ -1,6 +1,7 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-mapbox-gl" PKG_REQUIRE_NAME = "@kbn/mapbox-gl" @@ -26,17 +27,23 @@ NPM_MODULE_EXTRA_FILES = [ "README.md" ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "@npm//@mapbox/mapbox-gl-rtl-text", "@npm//file-loader", "@npm//mapbox-gl", ] TYPES_DEPS = [ + "@npm//@mapbox/mapbox-gl-rtl-text", + "@npm//file-loader", "@npm//@types/mapbox-gl", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -48,13 +55,14 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - out_dir = "target", + emit_declaration_only = True, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -63,7 +71,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-mapbox-gl/package.json b/packages/kbn-mapbox-gl/package.json index 9de88dac54a5a..fef881f2631da 100644 --- a/packages/kbn-mapbox-gl/package.json +++ b/packages/kbn-mapbox-gl/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts" + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts" } diff --git a/packages/kbn-mapbox-gl/src/index.ts b/packages/kbn-mapbox-gl/src/index.ts index 404684af78031..87b85002d7598 100644 --- a/packages/kbn-mapbox-gl/src/index.ts +++ b/packages/kbn-mapbox-gl/src/index.ts @@ -35,8 +35,9 @@ import 'mapbox-gl/dist/mapbox-gl.css'; mapboxgl.workerUrl = mbWorkerUrl; mapboxgl.setRTLTextPlugin(mbRtlPlugin); -export { - mapboxgl, +export { mapboxgl }; + +export type { Map, GeoJSONSource, VectorSource, diff --git a/packages/kbn-mapbox-gl/tsconfig.json b/packages/kbn-mapbox-gl/tsconfig.json index 159f40c6a7ca6..e935276e91762 100644 --- a/packages/kbn-mapbox-gl/tsconfig.json +++ b/packages/kbn-mapbox-gl/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "outDir": "./target/types", "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-mapbox-gl/src", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 48130a7bfcf5b..b11458d6539e8 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -50,15 +50,13 @@ pageLoadAssetSize: lists: 22900 logstash: 53548 management: 46112 - maps: 80000 - mapsLegacy: 87859 + maps: 90000 ml: 82187 monitoring: 80000 navigation: 37269 newsfeed: 42228 observability: 89709 painlessLab: 179748 - regionMap: 66098 remoteClusters: 51327 reporting: 183418 rollup: 97204 @@ -75,7 +73,6 @@ pageLoadAssetSize: spaces: 57868 telemetry: 51957 telemetryManagementSection: 38586 - tileMap: 65337 timelion: 29920 transform: 41007 triggersActionsUi: 100000 diff --git a/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts b/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts index 719301bce8e06..48f9d705da27d 100644 --- a/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts +++ b/packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts @@ -31,7 +31,7 @@ export const mapConsumerToIndexName: Record = logs: '.alerts-observability.logs', infrastructure: '.alerts-observability.metrics', observability: '.alerts-observability', - siem: ['.alerts-security.alerts', '.siem-signals'], + siem: '.alerts-security.alerts', uptime: '.alerts-observability.uptime', }; export type ValidFeatureId = keyof typeof mapConsumerToIndexName; diff --git a/src/plugins/maps_legacy/config.ts b/packages/kbn-rule-data-utils/src/alerts_as_data_severity.ts similarity index 64% rename from src/plugins/maps_legacy/config.ts rename to packages/kbn-rule-data-utils/src/alerts_as_data_severity.ts index 41387a07d27cb..c23af291fbefc 100644 --- a/src/plugins/maps_legacy/config.ts +++ b/packages/kbn-rule-data-utils/src/alerts_as_data_severity.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { schema, TypeOf } from '@kbn/config-schema'; - -export const configSchema = schema.object({}); - -export type MapsLegacyConfig = TypeOf; +export const ALERT_SEVERITY_WARNING = 'warning'; +export const ALERT_SEVERITY_CRITICAL = 'critical'; +export type AlertSeverity = typeof ALERT_SEVERITY_WARNING | typeof ALERT_SEVERITY_CRITICAL; diff --git a/packages/kbn-rule-data-utils/src/index.ts b/packages/kbn-rule-data-utils/src/index.ts index f60ad31286c9c..ef06d5777b5ab 100644 --- a/packages/kbn-rule-data-utils/src/index.ts +++ b/packages/kbn-rule-data-utils/src/index.ts @@ -8,3 +8,4 @@ export * from './technical_field_names'; export * from './alerts_as_data_rbac'; +export * from './alerts_as_data_severity'; diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 4b3f3fbb6f370..fa3d61d00529c 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -32,8 +32,6 @@ const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; const ALERT_REASON = `${ALERT_NAMESPACE}.reason` as const; const ALERT_RISK_SCORE = `${ALERT_NAMESPACE}.risk_score` as const; const ALERT_SEVERITY = `${ALERT_NAMESPACE}.severity` as const; -const ALERT_SEVERITY_LEVEL = `${ALERT_NAMESPACE}.severity.level` as const; -const ALERT_SEVERITY_VALUE = `${ALERT_NAMESPACE}.severity.value` as const; const ALERT_START = `${ALERT_NAMESPACE}.start` as const; const ALERT_STATUS = `${ALERT_NAMESPACE}.status` as const; const ALERT_SYSTEM_STATUS = `${ALERT_NAMESPACE}.system_status` as const; @@ -49,7 +47,6 @@ const ALERT_RULE_CREATED_BY = `${ALERT_RULE_NAMESPACE}.created_by` as const; const ALERT_RULE_DESCRIPTION = `${ALERT_RULE_NAMESPACE}.description` as const; const ALERT_RULE_ENABLED = `${ALERT_RULE_NAMESPACE}.enabled` as const; const ALERT_RULE_FROM = `${ALERT_RULE_NAMESPACE}.from` as const; -const ALERT_RULE_ID = `${ALERT_RULE_NAMESPACE}.id` as const; const ALERT_RULE_INTERVAL = `${ALERT_RULE_NAMESPACE}.interval` as const; const ALERT_RULE_LICENSE = `${ALERT_RULE_NAMESPACE}.license` as const; const ALERT_RULE_CATEGORY = `${ALERT_RULE_NAMESPACE}.category` as const; @@ -108,7 +105,6 @@ const fields = { ALERT_RULE_DESCRIPTION, ALERT_RULE_ENABLED, ALERT_RULE_FROM, - ALERT_RULE_ID, ALERT_RULE_INTERVAL, ALERT_RULE_LICENSE, ALERT_RULE_NAME, @@ -129,8 +125,6 @@ const fields = { ALERT_RULE_VERSION, ALERT_START, ALERT_SEVERITY, - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, ALERT_STATUS, ALERT_SYSTEM_STATUS, ALERT_UUID, @@ -166,7 +160,6 @@ export { ALERT_RULE_DESCRIPTION, ALERT_RULE_ENABLED, ALERT_RULE_FROM, - ALERT_RULE_ID, ALERT_RULE_INTERVAL, ALERT_RULE_LICENSE, ALERT_RULE_NAME, @@ -186,8 +179,6 @@ export { ALERT_RULE_VERSION, ALERT_RULE_SEVERITY, ALERT_SEVERITY, - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, ALERT_START, ALERT_SYSTEM_STATUS, ALERT_UUID, diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 1f6b549c0110c..88075b66ad045 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -198,6 +198,8 @@ export class DocLinksService { transportSettings: `${ELASTICSEARCH_DOCS}modules-network.html#common-network-settings`, typesRemoval: `${ELASTICSEARCH_DOCS}removal-of-types.html`, deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`, + setupUpgrade: `${ELASTICSEARCH_DOCS}setup-upgrade.html`, + releaseHighlights: `${ELASTICSEARCH_DOCS}release-highlights.html`, }, siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, diff --git a/src/core/server/core_route_handler_context.test.ts b/src/core/server/core_route_handler_context.test.ts index 3a54f7d55410e..ace0144eae54f 100644 --- a/src/core/server/core_route_handler_context.test.ts +++ b/src/core/server/core_route_handler_context.test.ts @@ -44,43 +44,6 @@ describe('#elasticsearch', () => { expect(client2).toBe(mockResult); }); }); - - describe('#legacy', () => { - describe('#client', () => { - test('returns the results of coreStart.elasticsearch.legacy.client.asScoped', () => { - const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); - const context = new CoreRouteHandlerContext(coreStart, request); - - const client = context.elasticsearch.legacy.client; - expect(client).toBe(coreStart.elasticsearch.legacy.client.asScoped.mock.results[0].value); - }); - - test('lazily created', () => { - const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); - const context = new CoreRouteHandlerContext(coreStart, request); - - expect(coreStart.elasticsearch.legacy.client.asScoped).not.toHaveBeenCalled(); - const client = context.elasticsearch.legacy.client; - expect(coreStart.elasticsearch.legacy.client.asScoped).toHaveBeenCalled(); - expect(client).toBeDefined(); - }); - - test('only creates one instance', () => { - const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); - const context = new CoreRouteHandlerContext(coreStart, request); - - const client1 = context.elasticsearch.legacy.client; - const client2 = context.elasticsearch.legacy.client; - expect(coreStart.elasticsearch.legacy.client.asScoped.mock.calls.length).toBe(1); - const mockResult = coreStart.elasticsearch.legacy.client.asScoped.mock.results[0].value; - expect(client1).toBe(mockResult); - expect(client2).toBe(mockResult); - }); - }); - }); }); describe('#savedObjects', () => { diff --git a/src/core/server/core_route_handler_context.ts b/src/core/server/core_route_handler_context.ts index 1f79a24046d26..3106053eb6afa 100644 --- a/src/core/server/core_route_handler_context.ts +++ b/src/core/server/core_route_handler_context.ts @@ -15,19 +15,12 @@ import { ISavedObjectTypeRegistry, SavedObjectsClientProviderOptions, } from './saved_objects'; -import { - InternalElasticsearchServiceStart, - IScopedClusterClient, - LegacyScopedClusterClient, -} from './elasticsearch'; +import { InternalElasticsearchServiceStart, IScopedClusterClient } from './elasticsearch'; import { InternalUiSettingsServiceStart, IUiSettingsClient } from './ui_settings'; import { DeprecationsClient, InternalDeprecationsServiceStart } from './deprecations'; class CoreElasticsearchRouteHandlerContext { #client?: IScopedClusterClient; - #legacy?: { - client: Pick; - }; constructor( private readonly elasticsearchStart: InternalElasticsearchServiceStart, @@ -40,15 +33,6 @@ class CoreElasticsearchRouteHandlerContext { } return this.#client; } - - public get legacy() { - if (this.#legacy == null) { - this.#legacy = { - client: this.elasticsearchStart.legacy.client.asScoped(this.request), - }; - } - return this.#legacy; - } } class CoreSavedObjectsRouteHandlerContext { diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index 848d9c204bfbf..5e2bf784b2a1d 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -6,19 +6,23 @@ * Side Public License, v 1. */ -import { Client, ApiResponse } from '@elastic/elasticsearch'; +import type { Client, ApiResponse } from '@elastic/elasticsearch'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { ElasticsearchClient } from './types'; import { ICustomClusterClient } from './cluster_client'; +import { PRODUCT_RESPONSE_HEADER } from '../supported_server_response_check'; + +// use jest.requireActual() to prevent weird errors when people mock @elastic/elasticsearch +const { Client: UnmockedClient } = jest.requireActual('@elastic/elasticsearch'); const createInternalClientMock = ( res?: MockedTransportRequestPromise ): DeeplyMockedKeys => { // we mimic 'reflection' on a concrete instance of the client to generate the mocked functions. - const client = new Client({ + const client = new UnmockedClient({ node: 'http://localhost', - }) as any; + }); const omittedProps = [ '_events', @@ -142,7 +146,7 @@ export type MockedTransportRequestPromise = TransportRequestPromise & { const createSuccessTransportRequestPromise = ( body: T, { statusCode = 200 }: { statusCode?: number } = {}, - headers?: Record + headers: Record = { [PRODUCT_RESPONSE_HEADER]: 'Elasticsearch' } ): MockedTransportRequestPromise> => { const response = createApiResponse({ body, statusCode, headers }); const promise = Promise.resolve(response); @@ -163,7 +167,7 @@ function createApiResponse>( return { body: {} as any, statusCode: 200, - headers: {}, + headers: { [PRODUCT_RESPONSE_HEADER]: 'Elasticsearch' }, warnings: [], meta: {} as any, ...opts, diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index 0ccc0f51f6abd..8d70e0bcbd066 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -9,14 +9,12 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { ILegacyClusterClient, ILegacyCustomClusterClient } from './legacy'; import { elasticsearchClientMock, ClusterClientMock, CustomClusterClientMock, } from './client/mocks'; import { ElasticsearchClientConfig } from './client'; -import { legacyClientMock } from './legacy/mocks'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { @@ -32,8 +30,6 @@ type MockedElasticSearchServicePreboot = jest.Mocked; - createClient: jest.Mock; - client: jest.Mocked; }; } @@ -59,14 +55,8 @@ const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), - createClient: jest.fn(), - client: legacyClientMock.createClusterClient(), }, }; - setupContract.legacy.createClient.mockReturnValue(legacyClientMock.createCustomClusterClient()); - setupContract.legacy.client.asScoped.mockReturnValue( - legacyClientMock.createScopedClusterClient() - ); return setupContract; }; @@ -76,14 +66,9 @@ const createStartContractMock = () => { createClient: jest.fn(), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), - createClient: jest.fn(), - client: legacyClientMock.createClusterClient(), }, }; - startContract.legacy.createClient.mockReturnValue(legacyClientMock.createCustomClusterClient()); - startContract.legacy.client.asScoped.mockReturnValue( - legacyClientMock.createScopedClusterClient() - ); + startContract.createClient.mockImplementation(() => elasticsearchClientMock.createCustomClusterClient() ); @@ -92,11 +77,7 @@ const createStartContractMock = () => { const createInternalPrebootContractMock = createPrebootContractMock; -type MockedInternalElasticSearchServiceSetup = jest.Mocked< - InternalElasticsearchServiceSetup & { - legacy: { client: jest.Mocked }; - } ->; +type MockedInternalElasticSearchServiceSetup = jest.Mocked; const createInternalSetupContractMock = () => { const setupContract: MockedInternalElasticSearchServiceSetup = { esNodesCompatibility$: new BehaviorSubject({ @@ -113,9 +94,6 @@ const createInternalSetupContractMock = () => { ...createSetupContractMock().legacy, }, }; - setupContract.legacy.client.asScoped.mockReturnValue( - legacyClientMock.createScopedClusterClient() - ); return setupContract; }; @@ -144,10 +122,6 @@ export const elasticsearchServiceMock = { createSetup: createSetupContractMock, createInternalStart: createInternalStartContractMock, createStart: createStartContractMock, - createLegacyClusterClient: legacyClientMock.createClusterClient, - createLegacyCustomClusterClient: legacyClientMock.createCustomClusterClient, - createLegacyScopedClusterClient: legacyClientMock.createScopedClusterClient, - createLegacyElasticsearchClient: legacyClientMock.createElasticsearchClient, ...elasticsearchClientMock, }; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.mocks.ts b/src/core/server/elasticsearch/elasticsearch_service.test.mocks.ts index e42fe76e1d992..b1a60019a801f 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.mocks.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.mocks.ts @@ -6,8 +6,5 @@ * Side Public License, v 1. */ -export const MockLegacyClusterClient = jest.fn(); -jest.mock('./legacy/cluster_client', () => ({ LegacyClusterClient: MockLegacyClusterClient })); - export const MockClusterClient = jest.fn(); jest.mock('./client/cluster_client', () => ({ ClusterClient: MockClusterClient })); diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 8932a4c73e1f2..2f1883fd8646a 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { MockLegacyClusterClient, MockClusterClient } from './elasticsearch_service.test.mocks'; +import { MockClusterClient } from './elasticsearch_service.test.mocks'; import { BehaviorSubject } from 'rxjs'; import { first } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/dev-utils'; @@ -18,7 +18,6 @@ import { httpServiceMock } from '../http/http_service.mock'; import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; import { configSchema, ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; -import { elasticsearchServiceMock } from './elasticsearch_service.mock'; import { elasticsearchClientMock } from './client/mocks'; import { duration } from 'moment'; @@ -37,9 +36,7 @@ let coreContext: CoreContext; const logger = loggingSystemMock.create(); let mockClusterClientInstance: ReturnType; -let mockLegacyClusterClientInstance: ReturnType< - typeof elasticsearchServiceMock.createLegacyCustomClusterClient ->; + let mockConfig$: BehaviorSubject; beforeEach(() => { env = Env.createDefault(REPO_ROOT, getEnvOptions()); @@ -58,11 +55,7 @@ beforeEach(() => { coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; elasticsearchService = new ElasticsearchService(coreContext); - MockLegacyClusterClient.mockClear(); MockClusterClient.mockClear(); - - mockLegacyClusterClientInstance = elasticsearchServiceMock.createLegacyCustomClusterClient(); - MockLegacyClusterClient.mockImplementation(() => mockLegacyClusterClientInstance); mockClusterClientInstance = elasticsearchClientMock.createCustomClusterClient(); MockClusterClient.mockImplementation(() => mockClusterClientInstance); }); @@ -162,141 +155,6 @@ describe('#setup', () => { ); }); - it('returns legacy elasticsearch client as a part of the contract', async () => { - const setupContract = await elasticsearchService.setup(setupDeps); - const client = setupContract.legacy.client; - - expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); - await client.callAsInternalUser('any'); - expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); - }); - - describe('#createLegacyClient', () => { - it('allows to specify config properties', async () => { - const setupContract = await elasticsearchService.setup(setupDeps); - - // reset all mocks called during setup phase - MockLegacyClusterClient.mockClear(); - - const customConfig = { keepAlive: true }; - const clusterClient = setupContract.legacy.createClient('some-custom-type', customConfig); - - expect(clusterClient).toBe(mockLegacyClusterClientInstance); - - expect(MockLegacyClusterClient).toHaveBeenCalledWith( - expect.objectContaining(customConfig), - expect.objectContaining({ context: ['elasticsearch'] }), - 'some-custom-type', - expect.any(Function) - ); - }); - - it('falls back to elasticsearch default config values if property not specified', async () => { - const setupContract = await elasticsearchService.setup(setupDeps); - - // reset all mocks called during setup phase - MockLegacyClusterClient.mockClear(); - - const customConfig = { - hosts: ['http://8.8.8.8'], - logQueries: true, - ssl: { certificate: 'certificate-value' }, - }; - setupContract.legacy.createClient('some-custom-type', customConfig); - - const config = MockLegacyClusterClient.mock.calls[0][0]; - expect(config).toMatchInlineSnapshot(` - Object { - "healthCheckDelay": "PT0.01S", - "hosts": Array [ - "http://8.8.8.8", - ], - "logQueries": true, - "requestHeadersWhitelist": Array [ - undefined, - ], - "ssl": Object { - "certificate": "certificate-value", - "verificationMode": "none", - }, - } - `); - }); - it('falls back to elasticsearch config if custom config not passed', async () => { - const setupContract = await elasticsearchService.setup(setupDeps); - - // reset all mocks called during setup phase - MockLegacyClusterClient.mockClear(); - - setupContract.legacy.createClient('another-type'); - - const config = MockLegacyClusterClient.mock.calls[0][0]; - expect(config).toMatchInlineSnapshot(` - Object { - "healthCheckDelay": "PT0.01S", - "hosts": Array [ - "http://1.2.3.4", - ], - "requestHeadersWhitelist": Array [ - undefined, - ], - "ssl": Object { - "alwaysPresentCertificate": undefined, - "certificate": undefined, - "certificateAuthorities": undefined, - "key": undefined, - "keyPassphrase": undefined, - "verificationMode": "none", - }, - } - `); - }); - - it('does not merge elasticsearch hosts if custom config overrides', async () => { - configService.atPath.mockReturnValueOnce( - new BehaviorSubject({ - hosts: ['http://1.2.3.4', 'http://9.8.7.6'], - healthCheck: { - delay: duration(2000), - }, - ssl: { - verificationMode: 'none', - }, - } as any) - ); - elasticsearchService = new ElasticsearchService(coreContext); - const setupContract = await elasticsearchService.setup(setupDeps); - - // reset all mocks called during setup phase - MockLegacyClusterClient.mockClear(); - - const customConfig = { - hosts: ['http://8.8.8.8'], - logQueries: true, - ssl: { certificate: 'certificate-value' }, - }; - setupContract.legacy.createClient('some-custom-type', customConfig); - - const config = MockLegacyClusterClient.mock.calls[0][0]; - expect(config).toMatchInlineSnapshot(` - Object { - "healthCheckDelay": "PT2S", - "hosts": Array [ - "http://8.8.8.8", - ], - "logQueries": true, - "requestHeadersWhitelist": Array [ - undefined, - ], - "ssl": Object { - "certificate": "certificate-value", - "verificationMode": "none", - }, - } - `); - }); - }); - it('esNodeVersionCompatibility$ only starts polling when subscribed to', async (done) => { const mockedClient = mockClusterClientInstance.asInternalUser; mockedClient.nodes.info.mockImplementation(() => @@ -419,7 +277,6 @@ describe('#stop', () => { await elasticsearchService.start(); await elasticsearchService.stop(); - expect(mockLegacyClusterClientInstance.close).toHaveBeenCalledTimes(1); expect(mockClusterClientInstance.close).toHaveBeenCalledTimes(1); }); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index acd2204334c0e..ce48f49b68660 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -13,11 +13,7 @@ import { merge } from '@kbn/std'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; -import { - LegacyClusterClient, - ILegacyCustomClusterClient, - LegacyElasticsearchClientConfig, -} from './legacy'; + import { ClusterClient, ElasticsearchClientConfig } from './client'; import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; import type { InternalHttpServiceSetup, GetAuthHeaders } from '../http'; @@ -45,12 +41,6 @@ export class ElasticsearchService private getAuthHeaders?: GetAuthHeaders; private executionContextClient?: IExecutionContext; - private createLegacyCustomClient?: ( - type: string, - clientConfig?: Partial - ) => ILegacyCustomClusterClient; - private legacyClient?: LegacyClusterClient; - private client?: ClusterClient; constructor(private readonly coreContext: CoreContext) { @@ -84,7 +74,6 @@ export class ElasticsearchService this.getAuthHeaders = deps.http.getAuthHeaders; this.executionContextClient = deps.executionContext; - this.legacyClient = this.createLegacyClusterClient('data', config); this.client = this.createClusterClient('data', config); const esNodesCompatibility$ = pollEsNodesVersion({ @@ -95,23 +84,16 @@ export class ElasticsearchService kibanaVersion: this.kibanaVersion, }).pipe(takeUntil(this.stop$), shareReplay({ refCount: true, bufferSize: 1 })); - this.createLegacyCustomClient = (type, clientConfig = {}) => { - const finalConfig = merge({}, config, clientConfig); - return this.createLegacyClusterClient(type, finalConfig); - }; - return { legacy: { config$: this.config$, - client: this.legacyClient, - createClient: this.createLegacyCustomClient, }, esNodesCompatibility$, status$: calculateStatus$(esNodesCompatibility$), }; } public async start(): Promise { - if (!this.legacyClient || !this.createLegacyCustomClient) { + if (!this.client) { throw new Error('ElasticsearchService needs to be setup before calling start'); } @@ -121,8 +103,6 @@ export class ElasticsearchService createClient: (type, clientConfig) => this.createClusterClient(type, config, clientConfig), legacy: { config$: this.config$, - client: this.legacyClient, - createClient: this.createLegacyCustomClient, }, }; } @@ -133,9 +113,6 @@ export class ElasticsearchService if (this.client) { await this.client.close(); } - if (this.legacyClient) { - this.legacyClient.close(); - } } private createClusterClient( @@ -152,13 +129,4 @@ export class ElasticsearchService () => this.executionContextClient?.getAsHeader() ); } - - private createLegacyClusterClient(type: string, config: LegacyElasticsearchClientConfig) { - return new LegacyClusterClient( - config, - this.coreContext.logger.get('elasticsearch'), - type, - this.getAuthHeaders - ); - } } diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index 62bb30452bb98..7f0620a03e5f4 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -22,7 +22,6 @@ export type { ScopeableRequest, ElasticsearchConfigPreboot, } from './types'; -export * from './legacy'; export type { IClusterClient, ICustomClusterClient, @@ -38,4 +37,8 @@ export type { DeleteDocumentResponse, } from './client'; export { getRequestDebugMeta, getErrorMessage } from './client'; -export { isSupportedEsServer } from './supported_server_response_check'; +export { + isSupportedEsServer, + isNotFoundFromUnsupportedServer, + PRODUCT_RESPONSE_HEADER, +} from './supported_server_response_check'; diff --git a/src/core/server/elasticsearch/legacy/api_types.ts b/src/core/server/elasticsearch/legacy/api_types.ts deleted file mode 100644 index e4ff4816527b4..0000000000000 --- a/src/core/server/elasticsearch/legacy/api_types.ts +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - Client, - GenericParams, - // root params - BulkIndexDocumentsParams, - ClearScrollParams, - CountParams, - CreateDocumentParams, - DeleteDocumentParams, - DeleteDocumentByQueryParams, - DeleteScriptParams, - DeleteTemplateParams, - ExistsParams, - ExplainParams, - FieldStatsParams, - GetParams, - GetResponse, - GetScriptParams, - GetSourceParams, - GetTemplateParams, - IndexDocumentParams, - InfoParams, - MGetParams, - MSearchParams, - MSearchTemplateParams, - MTermVectorsParams, - PingParams, - PutScriptParams, - PutTemplateParams, - ReindexParams, - ReindexRethrottleParams, - RenderSearchTemplateParams, - ScrollParams, - SearchParams, - SearchShardsParams, - SearchTemplateParams, - SuggestParams, - TermvectorsParams, - UpdateDocumentParams, - UpdateDocumentByQueryParams, - MGetResponse, - MSearchResponse, - SearchResponse, - // cat - CatAliasesParams, - CatAllocationParams, - CatFielddataParams, - CatHealthParams, - CatHelpParams, - CatIndicesParams, - CatCommonParams, - CatRecoveryParams, - CatSegmentsParams, - CatShardsParams, - CatSnapshotsParams, - CatTasksParams, - CatThreadPoolParams, - // cluster - ClusterAllocationExplainParams, - ClusterGetSettingsParams, - ClusterHealthParams, - ClusterPendingTasksParams, - ClusterPutSettingsParams, - ClusterRerouteParams, - ClusterStateParams, - ClusterStatsParams, - // indices - IndicesAnalyzeParams, - IndicesClearCacheParams, - IndicesCloseParams, - IndicesCreateParams, - IndicesDeleteParams, - IndicesDeleteAliasParams, - IndicesDeleteTemplateParams, - IndicesExistsParams, - IndicesExistsAliasParams, - IndicesExistsTemplateParams, - IndicesExistsTypeParams, - IndicesFlushParams, - IndicesFlushSyncedParams, - IndicesForcemergeParams, - IndicesGetParams, - IndicesGetAliasParams, - IndicesGetFieldMappingParams, - IndicesGetMappingParams, - IndicesGetSettingsParams, - IndicesGetTemplateParams, - IndicesGetUpgradeParams, - IndicesOpenParams, - IndicesPutAliasParams, - IndicesPutMappingParams, - IndicesPutSettingsParams, - IndicesPutTemplateParams, - IndicesRecoveryParams, - IndicesRefreshParams, - IndicesRolloverParams, - IndicesSegmentsParams, - IndicesShardStoresParams, - IndicesShrinkParams, - IndicesStatsParams, - IndicesUpdateAliasesParams, - IndicesUpgradeParams, - IndicesValidateQueryParams, - // ingest - IngestDeletePipelineParams, - IngestGetPipelineParams, - IngestPutPipelineParams, - IngestSimulateParams, - // nodes - NodesHotThreadsParams, - NodesInfoParams, - NodesStatsParams, - // snapshot - SnapshotCreateParams, - SnapshotCreateRepositoryParams, - SnapshotDeleteParams, - SnapshotDeleteRepositoryParams, - SnapshotGetParams, - SnapshotGetRepositoryParams, - SnapshotRestoreParams, - SnapshotStatusParams, - SnapshotVerifyRepositoryParams, - // tasks - TasksCancelParams, - TasksGetParams, - TasksListParams, -} from 'elasticsearch'; - -/** - * The set of options that defines how API call should be made and result be - * processed. - * - * @public - * @deprecated - * @removeBy 7.16 - */ -export interface LegacyCallAPIOptions { - /** - * Indicates whether `401 Unauthorized` errors returned from the Elasticsearch API - * should be wrapped into `Boom` error instances with properly set `WWW-Authenticate` - * header that could have been returned by the API itself. If API didn't specify that - * then `Basic realm="Authorization Required"` is used as `WWW-Authenticate`. - */ - wrap401Errors?: boolean; - /** - * A signal object that allows you to abort the request via an AbortController object. - */ - signal?: AbortSignal; -} - -/** - * @deprecated - * @removeBy 7.16 - * @public - * */ -export interface LegacyAPICaller { - /* eslint-disable */ - (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'clearScroll', params: ClearScrollParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'count', params: CountParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'create', params: CreateDocumentParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'delete', params: DeleteDocumentParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'deleteScript', params: DeleteScriptParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'exists', params: ExistsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'explain', params: ExplainParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'fieldStats', params: FieldStatsParams, options?: LegacyCallAPIOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'get', params: GetParams, options?: LegacyCallAPIOptions): Promise>; - (endpoint: 'getScript', params: GetScriptParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'getSource', params: GetSourceParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'getTemplate', params: GetTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'index', params: IndexDocumentParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'info', params: InfoParams, options?: LegacyCallAPIOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'mget', params: MGetParams, options?: LegacyCallAPIOptions): Promise>; - (endpoint: 'msearch', params: MSearchParams, options?: LegacyCallAPIOptions): Promise>; - (endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: LegacyCallAPIOptions): Promise>; - (endpoint: 'mtermvectors', params: MTermVectorsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'ping', params: PingParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'putScript', params: PutScriptParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'putTemplate', params: PutTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'reindex', params: ReindexParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'scroll', params: ScrollParams, options?: LegacyCallAPIOptions): Promise>; - (endpoint: 'search', params: SearchParams, options?: LegacyCallAPIOptions): Promise>; - (endpoint: 'searchShards', params: SearchShardsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'searchTemplate', params: SearchTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'suggest', params: SuggestParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'termvectors', params: TermvectorsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'update', params: UpdateDocumentParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: LegacyCallAPIOptions): ReturnType; - - // cat namespace - (endpoint: 'cat.aliases', params: CatAliasesParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.allocation', params: CatAllocationParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.count', params: CatAllocationParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.fielddata', params: CatFielddataParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.health', params: CatHealthParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.help', params: CatHelpParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.indices', params: CatIndicesParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.master', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.nodes', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.plugins', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.recovery', params: CatRecoveryParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.repositories', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.segments', params: CatSegmentsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.shards', params: CatShardsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.tasks', params: CatTasksParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: LegacyCallAPIOptions): ReturnType; - - // cluster namespace - (endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cluster.health', params: ClusterHealthParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cluster.state', params: ClusterStateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'cluster.stats', params: ClusterStatsParams, options?: LegacyCallAPIOptions): ReturnType; - - // indices namespace - (endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.close', params: IndicesCloseParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.create', params: IndicesCreateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.delete', params: IndicesDeleteParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.exists', params: IndicesExistsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.flush', params: IndicesFlushParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.get', params: IndicesGetParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.open', params: IndicesOpenParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.stats', params: IndicesStatsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: LegacyCallAPIOptions): ReturnType; - - // ingest namepsace - (endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: LegacyCallAPIOptions): ReturnType; - - // nodes namespace - (endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'nodes.info', params: NodesInfoParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'nodes.stats', params: NodesStatsParams, options?: LegacyCallAPIOptions): ReturnType; - - // snapshot namespace - (endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'snapshot.get', params: SnapshotGetParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: LegacyCallAPIOptions): ReturnType; - - // tasks namespace - (endpoint: 'tasks.cancel', params: TasksCancelParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'tasks.get', params: TasksGetParams, options?: LegacyCallAPIOptions): ReturnType; - (endpoint: 'tasks.list', params: TasksListParams, options?: LegacyCallAPIOptions): ReturnType; - - // other APIs accessed via transport.request - (endpoint: 'transport.request', clientParams: AssistantAPIClientParams, options?: LegacyCallAPIOptions): Promise< - AssistanceAPIResponse - >; - (endpoint: 'transport.request', clientParams: DeprecationAPIClientParams, options?: LegacyCallAPIOptions): Promise< - DeprecationAPIResponse - >; - - // Catch-all definition - (endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; - /* eslint-enable */ -} - -/** - * @deprecated - * @removeBy 7.16 - * @public - * */ -export interface AssistantAPIClientParams extends GenericParams { - path: '/_migration/assistance'; - method: 'GET'; -} - -/** - * @deprecated - * @removeBy 7.16 - * @public - * */ -export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -/** - * @deprecated - * @removeBy 7.16 - * @public - * */ -export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; - -/** - * @deprecated - * @removeBy 7.16 - * @public - * */ -export interface AssistanceAPIResponse { - indices: { - [indexName: string]: { - action_required: MIGRATION_ASSISTANCE_INDEX_ACTION; - }; - }; -} - -/** - * @deprecated - * @removeBy 7.16 - * @public - * */ -export interface DeprecationAPIClientParams extends GenericParams { - path: '/_migration/deprecations'; - method: 'GET'; -} - -/** - * @deprecated - * @removeBy 7.16 - * @public - * */ -export interface DeprecationInfo { - level: MIGRATION_DEPRECATION_LEVEL; - message: string; - url: string; - details?: string; -} - -/** - * @deprecated - * @removeBy 7.16 - * @public - * */ -export interface IndexSettingsDeprecationInfo { - [indexName: string]: DeprecationInfo[]; -} - -/** - * @deprecated - * @removeBy 7.16 - * @public - * */ -export interface DeprecationAPIResponse { - cluster_settings: DeprecationInfo[]; - ml_settings: DeprecationInfo[]; - node_settings: DeprecationInfo[]; - index_settings: IndexSettingsDeprecationInfo; -} diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.mocks.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.mocks.ts deleted file mode 100644 index 73f0fb3216eee..0000000000000 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.mocks.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const MockClient = jest.fn(); -jest.mock('elasticsearch', () => { - const original = jest.requireActual('elasticsearch'); - - return { - ...original, - Client: MockClient, - }; -}); - -export const MockScopedClusterClient = jest.fn(); -jest.mock('./scoped_cluster_client', () => ({ - LegacyScopedClusterClient: MockScopedClusterClient, -})); - -export const mockParseElasticsearchClientConfig = jest.fn(); -jest.mock('./elasticsearch_client_config', () => ({ - parseElasticsearchClientConfig: mockParseElasticsearchClientConfig, -})); diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts deleted file mode 100644 index 52bc4bd45660e..0000000000000 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts +++ /dev/null @@ -1,547 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ElasticsearchConfig } from '../elasticsearch_config'; - -import { - MockClient, - mockParseElasticsearchClientConfig, - MockScopedClusterClient, -} from './cluster_client.test.mocks'; - -import { errors } from 'elasticsearch'; -import { get } from 'lodash'; -import { Logger } from '../../logging'; -import { loggingSystemMock } from '../../logging/logging_system.mock'; -import { httpServerMock } from '../../http/http_server.mocks'; -import { LegacyClusterClient } from './cluster_client'; - -const logger = loggingSystemMock.create(); -afterEach(() => jest.clearAllMocks()); - -test('#constructor creates client with parsed config', () => { - const mockEsClientConfig = { apiVersion: 'es-client-master' }; - mockParseElasticsearchClientConfig.mockReturnValue(mockEsClientConfig); - - const mockEsConfig = { apiVersion: 'es-version' } as any; - const mockLogger = logger.get(); - - const clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); - expect(clusterClient).toBeDefined(); - - expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( - mockEsConfig, - mockLogger, - 'custom-type' - ); - - expect(MockClient).toHaveBeenCalledTimes(1); - expect(MockClient).toHaveBeenCalledWith(mockEsClientConfig); -}); - -describe('#callAsInternalUser', () => { - let mockEsClientInstance: { - close: jest.Mock; - ping: jest.Mock; - security: { authenticate: jest.Mock }; - }; - let clusterClient: LegacyClusterClient; - - beforeEach(() => { - mockEsClientInstance = { - close: jest.fn(), - ping: jest.fn(), - security: { authenticate: jest.fn() }, - }; - MockClient.mockImplementation(() => mockEsClientInstance); - - clusterClient = new LegacyClusterClient( - { apiVersion: 'es-version' } as any, - logger.get(), - 'custom-type' - ); - }); - - test('fails if cluster client is closed', async () => { - clusterClient.close(); - - await expect( - clusterClient.callAsInternalUser('ping', {}) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cluster client cannot be used after it has been closed."` - ); - }); - - test('fails if endpoint is invalid', async () => { - await expect( - clusterClient.callAsInternalUser('pong', {}) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"called with an invalid endpoint: pong"`); - }); - - test('correctly deals with top level endpoint', async () => { - const mockResponse = { data: 'ping' }; - const mockParams = { param: 'ping' }; - mockEsClientInstance.ping.mockImplementation(function mockCall(this: any) { - return Promise.resolve({ - context: this, - response: mockResponse, - }); - }); - - const mockResult = await clusterClient.callAsInternalUser('ping', mockParams); - expect(mockResult.response).toBe(mockResponse); - expect(mockResult.context).toBe(mockEsClientInstance); - expect(mockEsClientInstance.ping).toHaveBeenCalledTimes(1); - expect(mockEsClientInstance.ping).toHaveBeenLastCalledWith(mockParams); - }); - - test('sets the authorization header when a service account token is configured', async () => { - clusterClient = new LegacyClusterClient( - { apiVersion: 'es-version', serviceAccountToken: 'ABC123' } as any, - logger.get(), - 'custom-type' - ); - - const mockResponse = { data: 'ping' }; - const mockParams = { param: 'ping' }; - mockEsClientInstance.ping.mockImplementation(function mockCall(this: any) { - return Promise.resolve({ - context: this, - response: mockResponse, - }); - }); - - await clusterClient.callAsInternalUser('ping', mockParams); - - expect(mockEsClientInstance.ping).toHaveBeenCalledWith({ - headers: { authorization: 'Bearer ABC123' }, - param: 'ping', - }); - }); - - test('correctly deals with nested endpoint', async () => { - const mockResponse = { data: 'authenticate' }; - const mockParams = { param: 'authenticate' }; - mockEsClientInstance.security.authenticate.mockImplementation(function mockCall(this: any) { - return Promise.resolve({ - context: this, - response: mockResponse, - }); - }); - - const mockResult = await clusterClient.callAsInternalUser('security.authenticate', mockParams); - expect(mockResult.response).toBe(mockResponse); - expect(mockResult.context).toBe(mockEsClientInstance.security); - expect(mockEsClientInstance.security.authenticate).toHaveBeenCalledTimes(1); - expect(mockEsClientInstance.security.authenticate).toHaveBeenLastCalledWith(mockParams); - }); - - test('does not wrap errors if `wrap401Errors` is set to `false`', async () => { - const mockError = { message: 'some error' }; - mockEsClientInstance.ping.mockRejectedValue(mockError); - - await expect( - clusterClient.callAsInternalUser('ping', undefined, { wrap401Errors: false }) - ).rejects.toBe(mockError); - - const mockAuthenticationError = { message: 'authentication error', statusCode: 401 }; - mockEsClientInstance.ping.mockRejectedValue(mockAuthenticationError); - - await expect( - clusterClient.callAsInternalUser('ping', undefined, { wrap401Errors: false }) - ).rejects.toBe(mockAuthenticationError); - }); - - test('wraps 401 errors when `wrap401Errors` is set to `true` or unspecified', async () => { - const mockError = { message: 'some error' }; - mockEsClientInstance.ping.mockRejectedValue(mockError); - - await expect(clusterClient.callAsInternalUser('ping')).rejects.toBe(mockError); - await expect( - clusterClient.callAsInternalUser('ping', undefined, { wrap401Errors: true }) - ).rejects.toBe(mockError); - - const mockAuthorizationError = { message: 'authentication error', statusCode: 403 }; - mockEsClientInstance.ping.mockRejectedValue(mockAuthorizationError); - - await expect(clusterClient.callAsInternalUser('ping')).rejects.toBe(mockAuthorizationError); - await expect( - clusterClient.callAsInternalUser('ping', undefined, { wrap401Errors: true }) - ).rejects.toBe(mockAuthorizationError); - - const mockAuthenticationError = new (errors.AuthenticationException as any)( - 'Authentication Exception', - { statusCode: 401 } - ); - mockEsClientInstance.ping.mockRejectedValue(mockAuthenticationError); - - await expect(clusterClient.callAsInternalUser('ping')).rejects.toBe(mockAuthenticationError); - await expect( - clusterClient.callAsInternalUser('ping', undefined, { wrap401Errors: true }) - ).rejects.toStrictEqual(mockAuthenticationError); - }); - - test('aborts the request and rejects if a signal is provided and aborted', async () => { - const controller = new AbortController(); - - // The ES client returns a promise with an additional `abort` method to abort the request - const mockValue: any = Promise.resolve(); - mockValue.abort = jest.fn(); - mockEsClientInstance.ping.mockReturnValue(mockValue); - - const promise = clusterClient.callAsInternalUser('ping', undefined, { - wrap401Errors: false, - signal: controller.signal, - }); - - controller.abort(); - - expect(mockValue.abort).toHaveBeenCalled(); - await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot(`"Request was aborted"`); - }); - - test('does not override WWW-Authenticate if returned by Elasticsearch', async () => { - const mockAuthenticationError = new (errors.AuthenticationException as any)( - 'Authentication Exception', - { statusCode: 401 } - ); - - const mockAuthenticationErrorWithHeader = new (errors.AuthenticationException as any)( - 'Authentication Exception', - { - body: { error: { header: { 'WWW-Authenticate': 'some custom header' } } }, - statusCode: 401, - } - ); - mockEsClientInstance.ping - .mockRejectedValueOnce(mockAuthenticationError) - .mockRejectedValueOnce(mockAuthenticationErrorWithHeader); - - await expect(clusterClient.callAsInternalUser('ping')).rejects.toBe(mockAuthenticationError); - expect(get(mockAuthenticationError, 'output.headers.WWW-Authenticate')).toBe( - 'Basic realm="Authorization Required"' - ); - - await expect(clusterClient.callAsInternalUser('ping')).rejects.toBe( - mockAuthenticationErrorWithHeader - ); - expect(get(mockAuthenticationErrorWithHeader, 'output.headers.WWW-Authenticate')).toBe( - 'some custom header' - ); - }); -}); - -describe('#asScoped', () => { - let mockEsClientInstance: { ping: jest.Mock; close: jest.Mock }; - let mockScopedEsClientInstance: { ping: jest.Mock; close: jest.Mock }; - - let clusterClient: LegacyClusterClient; - let mockLogger: Logger; - let mockEsConfig: ElasticsearchConfig; - - beforeEach(() => { - mockEsClientInstance = { ping: jest.fn(), close: jest.fn() }; - mockScopedEsClientInstance = { ping: jest.fn(), close: jest.fn() }; - MockClient.mockImplementationOnce(() => mockEsClientInstance).mockImplementationOnce( - () => mockScopedEsClientInstance - ); - - mockLogger = logger.get(); - mockEsConfig = { - apiVersion: 'es-version', - requestHeadersWhitelist: ['one', 'two'], - } as any; - - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); - jest.clearAllMocks(); - }); - - test('creates additional Elasticsearch client only once', () => { - const firstScopedClusterClient = clusterClient.asScoped( - httpServerMock.createRawRequest({ headers: { one: '1' } }) - ); - - expect(firstScopedClusterClient).toBeDefined(); - expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( - mockEsConfig, - mockLogger, - 'custom-type', - { - auth: false, - ignoreCertAndKey: true, - } - ); - - expect(MockClient).toHaveBeenCalledTimes(1); - expect(MockClient).toHaveBeenCalledWith( - mockParseElasticsearchClientConfig.mock.results[0].value - ); - - jest.clearAllMocks(); - - const secondScopedClusterClient = clusterClient.asScoped( - httpServerMock.createRawRequest({ headers: { two: '2' } }) - ); - - expect(secondScopedClusterClient).toBeDefined(); - expect(secondScopedClusterClient).not.toBe(firstScopedClusterClient); - expect(mockParseElasticsearchClientConfig).not.toHaveBeenCalled(); - expect(MockClient).not.toHaveBeenCalled(); - }); - - test('properly configures `ignoreCertAndKey` for various configurations', () => { - // Config without SSL. - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); - - mockParseElasticsearchClientConfig.mockClear(); - clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); - - expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( - mockEsConfig, - mockLogger, - 'custom-type', - { - auth: false, - ignoreCertAndKey: true, - } - ); - - // Config ssl.alwaysPresentCertificate === false - mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: false } } as any; - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); - - mockParseElasticsearchClientConfig.mockClear(); - clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); - - expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( - mockEsConfig, - mockLogger, - 'custom-type', - { - auth: false, - ignoreCertAndKey: true, - } - ); - - // Config ssl.alwaysPresentCertificate === true - mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: true } } as any; - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); - - mockParseElasticsearchClientConfig.mockClear(); - clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); - - expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); - expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith( - mockEsConfig, - mockLogger, - 'custom-type', - { - auth: false, - ignoreCertAndKey: false, - } - ); - }); - - test('passes only filtered headers to the scoped cluster client', () => { - clusterClient.asScoped( - httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) - ); - - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - { one: '1', two: '2' } - ); - }); - - test('passes x-opaque-id header with request id', () => { - clusterClient.asScoped( - httpServerMock.createKibanaRequest({ - kibanaRequestState: { requestId: 'alpha', requestUuid: 'ignore-this-id' }, - }) - ); - - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - { 'x-opaque-id': 'alpha' } - ); - }); - - test('does not set the authorization header when a service account token is configured', async () => { - clusterClient = new LegacyClusterClient( - { - apiVersion: 'es-version', - requestHeadersWhitelist: ['zero'], - serviceAccountToken: 'ABC123', - } as any, - logger.get(), - 'custom-type' - ); - - clusterClient.asScoped( - httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) - ); - - const expectedHeaders = { zero: '0' }; - - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - expectedHeaders - ); - }); - - test('both scoped and internal API caller fail if cluster client is closed', async () => { - clusterClient.asScoped( - httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) - ); - - clusterClient.close(); - - const [[internalAPICaller, scopedAPICaller]] = MockScopedClusterClient.mock.calls; - await expect(internalAPICaller('ping')).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cluster client cannot be used after it has been closed."` - ); - - await expect(scopedAPICaller('ping', {})).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cluster client cannot be used after it has been closed."` - ); - }); - - test('does not fail when scope to not defined request', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); - clusterClient.asScoped(); - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - {} - ); - }); - - test('does not fail when scope to a request without headers', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); - clusterClient.asScoped({} as any); - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - {} - ); - }); - - test('calls getAuthHeaders and filters results for a real request', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type', () => ({ - one: '1', - three: '3', - })); - clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { two: '2' } })); - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - { one: '1', two: '2' } - ); - }); - - test('getAuthHeaders results rewrite extends a request headers', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type', () => ({ - one: 'foo', - })); - clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1', two: '2' } })); - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - { one: 'foo', two: '2' } - ); - }); - - test("doesn't call getAuthHeaders for a fake request", async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type', () => ({})); - clusterClient.asScoped({ headers: { one: 'foo' } }); - - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - { one: 'foo' } - ); - }); - - test('filters a fake request headers', async () => { - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, 'custom-type'); - clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); - - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - { one: '1', two: '2' } - ); - }); -}); - -describe('#close', () => { - let mockEsClientInstance: { close: jest.Mock }; - let mockScopedEsClientInstance: { close: jest.Mock }; - - let clusterClient: LegacyClusterClient; - - beforeEach(() => { - mockEsClientInstance = { close: jest.fn() }; - mockScopedEsClientInstance = { close: jest.fn() }; - MockClient.mockImplementationOnce(() => mockEsClientInstance).mockImplementationOnce( - () => mockScopedEsClientInstance - ); - - clusterClient = new LegacyClusterClient( - { apiVersion: 'es-version', requestHeadersWhitelist: [] } as any, - logger.get(), - 'custom-type' - ); - }); - - test('closes underlying Elasticsearch client', () => { - expect(mockEsClientInstance.close).not.toHaveBeenCalled(); - - clusterClient.close(); - expect(mockEsClientInstance.close).toHaveBeenCalledTimes(1); - }); - - test('closes both internal and scoped underlying Elasticsearch clients', () => { - clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); - - expect(mockEsClientInstance.close).not.toHaveBeenCalled(); - expect(mockScopedEsClientInstance.close).not.toHaveBeenCalled(); - - clusterClient.close(); - expect(mockEsClientInstance.close).toHaveBeenCalledTimes(1); - expect(mockScopedEsClientInstance.close).toHaveBeenCalledTimes(1); - }); - - test('does not call close on already closed client', () => { - clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); - - clusterClient.close(); - mockEsClientInstance.close.mockClear(); - mockScopedEsClientInstance.close.mockClear(); - - clusterClient.close(); - expect(mockEsClientInstance.close).not.toHaveBeenCalled(); - expect(mockScopedEsClientInstance.close).not.toHaveBeenCalled(); - }); -}); diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts deleted file mode 100644 index 6a6765b67da9f..0000000000000 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Client } from 'elasticsearch'; -import { get } from 'lodash'; - -import { LegacyElasticsearchErrorHelpers } from './errors'; -import { GetAuthHeaders, isKibanaRequest, isRealRequest } from '../../http'; -import { filterHeaders, ensureRawRequest } from '../../http/router'; -import { Logger } from '../../logging'; -import { ScopeableRequest } from '../types'; -import { - LegacyElasticsearchClientConfig, - parseElasticsearchClientConfig, -} from './elasticsearch_client_config'; -import { LegacyScopedClusterClient, ILegacyScopedClusterClient } from './scoped_cluster_client'; -import { LegacyCallAPIOptions, LegacyAPICaller } from './api_types'; - -/** - * Support Legacy platform request for the period of migration. - * - * @public - */ - -const noop = () => undefined; - -/** - * Calls the Elasticsearch API endpoint with the specified parameters. - * @param client Raw Elasticsearch JS client instance to use. - * @param endpoint Name of the API endpoint to call. - * @param clientParams Parameters that will be directly passed to the - * Elasticsearch JS client. - * @param options Options that affect the way we call the API and process the result. - */ -const callAPI = async ( - client: Client, - endpoint: string, - clientParams: Record = {}, - options: LegacyCallAPIOptions = { wrap401Errors: true } -) => { - const clientPath = endpoint.split('.'); - const api: any = get(client, clientPath); - if (!api) { - throw new Error(`called with an invalid endpoint: ${endpoint}`); - } - - const apiContext = clientPath.length === 1 ? client : get(client, clientPath.slice(0, -1)); - try { - return await new Promise((resolve, reject) => { - const request = api.call(apiContext, clientParams); - if (options.signal) { - options.signal.addEventListener('abort', () => { - request.abort(); - reject(new Error('Request was aborted')); - }); - } - return request.then(resolve, reject); - }); - } catch (err) { - if (!options.wrap401Errors || err.statusCode !== 401) { - throw err; - } - - throw LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(err); - } -}; - -/** - * Represents an Elasticsearch cluster API client created by the platform. - * It allows to call API on behalf of the internal Kibana user and - * the actual user that is derived from the request headers (via `asScoped(...)`). - * - * See {@link LegacyClusterClient}. - * - * @deprecated Use {@link IClusterClient}. - * @removeBy 7.16 - * @public - */ -export type ILegacyClusterClient = Pick; - -/** - * Represents an Elasticsearch cluster API client created by a plugin. - * It allows to call API on behalf of the internal Kibana user and - * the actual user that is derived from the request headers (via `asScoped(...)`). - * - * See {@link LegacyClusterClient}. - * @deprecated Use {@link ICustomClusterClient}. - * @removeBy 7.16 - * @public - */ -export type ILegacyCustomClusterClient = Pick< - LegacyClusterClient, - 'callAsInternalUser' | 'close' | 'asScoped' ->; - -/** - * {@inheritDoc IClusterClient} - * @deprecated Use {@link IClusterClient}. - * @removeBy 7.16 - * @public - */ -export class LegacyClusterClient implements ILegacyClusterClient { - /** - * Raw Elasticsearch JS client that acts on behalf of the Kibana internal user. - */ - private readonly client: Client; - - /** - * Optional raw Elasticsearch JS client that is shared between all the scoped clients created - * from this cluster client. Every API call is attributed by the wh - */ - private scopedClient?: Client; - - /** - * Indicates whether this cluster client (and all internal raw Elasticsearch JS clients) has been closed. - */ - private isClosed = false; - - constructor( - private readonly config: LegacyElasticsearchClientConfig, - private readonly log: Logger, - private readonly type: string, - private readonly getAuthHeaders: GetAuthHeaders = noop - ) { - this.client = new Client(parseElasticsearchClientConfig(config, log, type)); - } - - /** - * Calls specified endpoint with provided clientParams on behalf of the - * Kibana internal user. - * See {@link LegacyAPICaller}. - * @deprecated Use {@link IClusterClient.asInternalUser}. - * - * @param endpoint - String descriptor of the endpoint e.g. `cluster.getSettings` or `ping`. - * @param clientParams - A dictionary of parameters that will be passed directly to the Elasticsearch JS client. - * @param options - Options that affect the way we call the API and process the result. - */ - public callAsInternalUser: LegacyAPICaller = async ( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) => { - this.assertIsNotClosed(); - - if (this.config.serviceAccountToken) { - clientParams.headers = { - ...clientParams.headers, - authorization: `Bearer ${this.config.serviceAccountToken}`, - }; - } - - return await (callAPI.bind(null, this.client) as LegacyAPICaller)( - endpoint, - clientParams, - options - ); - }; - - /** - * Closes the cluster client. After that client cannot be used and one should - * create a new client instance to be able to interact with Elasticsearch API. - */ - public close() { - if (this.isClosed) { - return; - } - - this.isClosed = true; - this.client.close(); - - if (this.scopedClient !== undefined) { - this.scopedClient.close(); - } - } - - /** - * Creates an instance of {@link ILegacyScopedClusterClient} based on the configuration the - * current cluster client that exposes additional `callAsCurrentUser` method - * scoped to the provided req. Consumers shouldn't worry about closing - * scoped client instances, these will be automatically closed as soon as the - * original cluster client isn't needed anymore and closed. - * - * @param request - Request the `IScopedClusterClient` instance will be scoped to. - * Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform - */ - public asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient { - // It'd have been quite expensive to create and configure client for every incoming - // request since it involves parsing of the config, reading of the SSL certificate and - // key files etc. Moreover scoped client needs two Elasticsearch JS clients at the same - // time: one to support `callAsInternalUser` and another one for `callAsCurrentUser`. - // To reduce that overhead we create one scoped client per cluster client and share it - // between all scoped client instances. - if (this.scopedClient === undefined) { - this.scopedClient = new Client( - parseElasticsearchClientConfig(this.config, this.log, this.type, { - auth: false, - ignoreCertAndKey: !this.config.ssl || !this.config.ssl.alwaysPresentCertificate, - }) - ); - } - - return new LegacyScopedClusterClient( - this.callAsInternalUser, - this.callAsCurrentUser, - filterHeaders(this.getHeaders(request), [ - 'x-opaque-id', - ...this.config.requestHeadersWhitelist, - ]) - ); - } - - /** - * Calls specified endpoint with provided clientParams on behalf of the - * user initiated request to the Kibana server (via HTTP request headers). - * See {@link LegacyAPICaller}. - * - * @param endpoint - String descriptor of the endpoint e.g. `cluster.getSettings` or `ping`. - * @param clientParams - A dictionary of parameters that will be passed directly to the Elasticsearch JS client. - * @param options - Options that affect the way we call the API and process the result. - */ - private callAsCurrentUser: LegacyAPICaller = async ( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) => { - this.assertIsNotClosed(); - - return await (callAPI.bind(null, this.scopedClient!) as LegacyAPICaller)( - endpoint, - clientParams, - options - ); - }; - - private assertIsNotClosed() { - if (this.isClosed) { - throw new Error('Cluster client cannot be used after it has been closed.'); - } - } - - private getHeaders(request?: ScopeableRequest): Record { - if (!isRealRequest(request)) { - return request && request.headers ? request.headers : {}; - } - const authHeaders = this.getAuthHeaders(request); - const requestHeaders = ensureRawRequest(request).headers; - const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {}; - - return { ...requestHeaders, ...requestIdHeaders, ...authHeaders }; - } -} diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts deleted file mode 100644 index a343c0d5d2ad1..0000000000000 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts +++ /dev/null @@ -1,756 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { duration } from 'moment'; -import { loggingSystemMock } from '../../logging/logging_system.mock'; -import { - LegacyElasticsearchClientConfig, - parseElasticsearchClientConfig, -} from './elasticsearch_client_config'; -import { DEFAULT_HEADERS } from '../default_headers'; -const logger = loggingSystemMock.create(); -afterEach(() => jest.clearAllMocks()); - -test('parses minimally specified config', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'master', - customHeaders: { xsrf: 'something' }, - sniffOnStart: false, - sniffOnConnectionFault: false, - hosts: ['http://localhost/elasticsearch'], - requestHeadersWhitelist: [], - }, - logger.get(), - 'custom-type' - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "master", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "localhost", - "path": "/elasticsearch", - "port": "80", - "protocol": "http:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": false, - "sniffOnStart": false, - } - `); -}); - -test('parses fully specified config', () => { - const elasticsearchConfig: LegacyElasticsearchClientConfig = { - apiVersion: 'v7.0.0', - customHeaders: { xsrf: 'something' }, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: [ - 'http://localhost/elasticsearch', - 'http://domain.com:1234/elasticsearch', - 'https://es.local', - ], - requestHeadersWhitelist: [], - username: 'elastic', - password: 'changeme', - pingTimeout: 12345, - requestTimeout: 54321, - sniffInterval: 11223344, - ssl: { - verificationMode: 'certificate', - certificateAuthorities: ['content-of-ca-path-1', 'content-of-ca-path-2'], - certificate: 'content-of-certificate-path', - key: 'content-of-key-path', - keyPassphrase: 'key-pass', - alwaysPresentCertificate: true, - }, - }; - - const elasticsearchClientConfig = parseElasticsearchClientConfig( - elasticsearchConfig, - logger.get(), - 'custom-type' - ); - - // Check that original references aren't used. - for (const host of elasticsearchClientConfig.hosts) { - expect(elasticsearchConfig.customHeaders).not.toBe(host.headers); - } - - expect(elasticsearchConfig.ssl).not.toBe(elasticsearchClientConfig.ssl); - - expect(elasticsearchClientConfig).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "localhost", - "path": "/elasticsearch", - "port": "80", - "protocol": "http:", - "query": null, - }, - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "domain.com", - "path": "/elasticsearch", - "port": "1234", - "protocol": "http:", - "query": null, - }, - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "httpAuth": "elastic:changeme", - "keepAlive": true, - "log": [Function], - "pingTimeout": 12345, - "requestTimeout": 54321, - "sniffInterval": 11223344, - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": Array [ - "content-of-ca-path-1", - "content-of-ca-path-2", - ], - "cert": "content-of-certificate-path", - "checkServerIdentity": [Function], - "key": "content-of-key-path", - "passphrase": "key-pass", - "rejectUnauthorized": true, - }, - } - `); -}); - -test('parses config timeouts of moment.Duration type', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'master', - customHeaders: { xsrf: 'something' }, - sniffOnStart: false, - sniffOnConnectionFault: false, - pingTimeout: duration(100, 'ms'), - requestTimeout: duration(30, 's'), - sniffInterval: duration(1, 'minute'), - hosts: ['http://localhost:9200/elasticsearch'], - requestHeadersWhitelist: [], - }, - logger.get(), - 'custom-type' - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "master", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "localhost", - "path": "/elasticsearch", - "port": "9200", - "protocol": "http:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "pingTimeout": 100, - "requestTimeout": 30000, - "sniffInterval": 60000, - "sniffOnConnectionFault": false, - "sniffOnStart": false, - } - `); -}); - -describe('#auth', () => { - test('is not set if #auth = false even if username and password are provided', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: { xsrf: 'something' }, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['http://user:password@localhost/elasticsearch', 'https://es.local'], - username: 'elastic', - password: 'changeme', - requestHeadersWhitelist: [], - }, - logger.get(), - 'custom-type', - { auth: false } - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "localhost", - "path": "/elasticsearch", - "port": "80", - "protocol": "http:", - "query": null, - }, - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - } - `); - }); - - test('is not set if username is not specified', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: { xsrf: 'something' }, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - password: 'changeme', - }, - logger.get(), - 'custom-type', - { auth: true } - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - } - `); - }); - - test('is not set if password is not specified', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: { xsrf: 'something' }, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - username: 'elastic', - }, - logger.get(), - 'custom-type', - { auth: true } - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - } - `); - }); -}); - -describe('#serviceAccountToken', () => { - it('is set when #auth is true, and a token is provided', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: { xsrf: 'something' }, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - serviceAccountToken: 'ABC123', - }, - logger.get(), - 'custom-type', - { auth: true } - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "serviceAccountToken": "ABC123", - "sniffOnConnectionFault": true, - "sniffOnStart": true, - } - `); - }); - - it('is not set when #auth is true, and a token is not provided', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: { xsrf: 'something' }, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - }, - logger.get(), - 'custom-type', - { auth: true } - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - } - `); - }); - - it('is not set when #auth is false, and a token is provided', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: { xsrf: 'something' }, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - serviceAccountToken: 'ABC123', - }, - logger.get(), - 'custom-type', - { auth: false } - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - "xsrf": "something", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - } - `); - }); -}); - -describe('#customHeaders', () => { - test('override the default headers', () => { - const headerKey = Object.keys(DEFAULT_HEADERS)[0]; - const parsedConfig = parseElasticsearchClientConfig( - { - apiVersion: 'master', - customHeaders: { [headerKey]: 'foo' }, - sniffOnStart: false, - sniffOnConnectionFault: false, - hosts: ['http://localhost/elasticsearch'], - requestHeadersWhitelist: [], - }, - logger.get(), - 'custom-type' - ); - expect(parsedConfig.hosts[0].headers).toEqual({ - [headerKey]: 'foo', - }); - }); -}); - -describe('#log', () => { - test('default logger', () => { - const parsedConfig = parseElasticsearchClientConfig( - { - apiVersion: 'master', - customHeaders: { xsrf: 'something' }, - sniffOnStart: false, - sniffOnConnectionFault: false, - hosts: ['http://localhost/elasticsearch'], - requestHeadersWhitelist: [], - }, - logger.get(), - 'custom-type' - ); - - const esLogger = new parsedConfig.log(); - esLogger.error('some-error'); - esLogger.warning('some-warning'); - - esLogger.trace('METHOD', { path: '/some-path' }, '?query=2', 'unknown', '304'); - - esLogger.info('some-info'); - esLogger.debug('some-debug'); - - expect(typeof esLogger.close).toBe('function'); - - expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(` - Object { - "debug": Array [ - Array [ - "304 - METHOD /some-path - ?query=2", - ], - ], - "error": Array [ - Array [ - "some-error", - ], - ], - "fatal": Array [], - "info": Array [], - "log": Array [], - "trace": Array [], - "warn": Array [ - Array [ - "some-warning", - ], - ], - } - `); - }); - - test('custom logger', () => { - const customLogger = jest.fn(); - - const parsedConfig = parseElasticsearchClientConfig( - { - apiVersion: 'master', - customHeaders: { xsrf: 'something' }, - sniffOnStart: false, - sniffOnConnectionFault: false, - hosts: ['http://localhost/elasticsearch'], - requestHeadersWhitelist: [], - log: customLogger, - }, - logger.get(), - 'custom-type' - ); - - expect(parsedConfig.log).toBe(customLogger); - }); -}); - -describe('#ssl', () => { - test('#verificationMode = none', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: {}, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - ssl: { verificationMode: 'none' }, - }, - logger.get(), - 'custom-type' - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": undefined, - "rejectUnauthorized": false, - }, - } - `); - }); - - test('#verificationMode = certificate', () => { - const clientConfig = parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: {}, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - ssl: { verificationMode: 'certificate' }, - }, - logger.get(), - 'custom-type' - ); - - // `checkServerIdentity` shouldn't check hostname when verificationMode is certificate. - expect( - clientConfig.ssl!.checkServerIdentity!('right.com', { subject: { CN: 'wrong.com' } } as any) - ).toBeUndefined(); - - expect(clientConfig).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": undefined, - "checkServerIdentity": [Function], - "rejectUnauthorized": true, - }, - } - `); - }); - - test('#verificationMode = full', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: {}, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - ssl: { verificationMode: 'full' }, - }, - logger.get(), - 'custom-type' - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": undefined, - "rejectUnauthorized": true, - }, - } - `); - }); - - test('#verificationMode is unknown', () => { - expect(() => - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: {}, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - ssl: { verificationMode: 'misspelled' as any }, - }, - logger.get(), - 'custom-type' - ) - ).toThrowErrorMatchingInlineSnapshot(`"Unknown ssl verificationMode: misspelled"`); - }); - - test('#ignoreCertAndKey = true', () => { - expect( - parseElasticsearchClientConfig( - { - apiVersion: 'v7.0.0', - customHeaders: {}, - sniffOnStart: true, - sniffOnConnectionFault: true, - hosts: ['https://es.local'], - requestHeadersWhitelist: [], - ssl: { - verificationMode: 'certificate', - certificateAuthorities: ['content-of-ca-path'], - certificate: 'content-of-certificate-path', - key: 'content-of-key-path', - keyPassphrase: 'key-pass', - alwaysPresentCertificate: true, - }, - }, - logger.get(), - 'custom-type', - { ignoreCertAndKey: true } - ) - ).toMatchInlineSnapshot(` - Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "x-elastic-product-origin": "kibana", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": Array [ - "content-of-ca-path", - ], - "checkServerIdentity": [Function], - "rejectUnauthorized": true, - }, - } - `); - }); -}); diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts deleted file mode 100644 index 3d81caefad457..0000000000000 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ConfigOptions } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; -import { Duration } from 'moment'; -import { checkServerIdentity } from 'tls'; -import url from 'url'; -import { pick } from '@kbn/std'; -import { Logger } from '../../logging'; -import { ElasticsearchConfig } from '../elasticsearch_config'; -import { DEFAULT_HEADERS } from '../default_headers'; - -/** - * @privateRemarks Config that consumers can pass to the Elasticsearch JS client is complex and includes - * not only entries from standard `elasticsearch.*` yaml config, but also some Elasticsearch JS - * client specific options like `keepAlive` or `plugins` (that eventually will be deprecated). - * - * @deprecated - * @public - */ -export type LegacyElasticsearchClientConfig = Pick & - Pick< - ElasticsearchConfig, - | 'apiVersion' - | 'customHeaders' - | 'requestHeadersWhitelist' - | 'sniffOnStart' - | 'sniffOnConnectionFault' - | 'hosts' - | 'username' - | 'password' - | 'serviceAccountToken' - > & { - pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; - requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; - sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval']; - ssl?: Partial; - }; - -/** @internal */ -interface LegacyElasticsearchClientConfigOverrides { - /** - * If set to `true`, username and password from the config won't be used - * to access Elasticsearch API even if these are specified. - */ - auth?: boolean; - - /** - * If set to `true`, `ssl.key` and `ssl.certificate` provided through config won't - * be used to connect to Elasticsearch. - */ - ignoreCertAndKey?: boolean; -} - -// Original `ConfigOptions` defines `ssl: object` so we need something more specific. -/** @internal */ -type ExtendedConfigOptions = ConfigOptions & - Partial<{ - serviceAccountToken?: string; - ssl: Partial<{ - rejectUnauthorized: boolean; - checkServerIdentity: typeof checkServerIdentity; - ca: string[]; - cert: string; - key: string; - passphrase: string; - }>; - }>; - -/** @internal */ -export function parseElasticsearchClientConfig( - config: LegacyElasticsearchClientConfig, - log: Logger, - type: string, - { ignoreCertAndKey = false, auth = true }: LegacyElasticsearchClientConfigOverrides = {} -) { - const esClientConfig: ExtendedConfigOptions = { - keepAlive: true, - ...pick(config, [ - 'apiVersion', - 'sniffOnStart', - 'sniffOnConnectionFault', - 'keepAlive', - 'log', - 'plugins', - ]), - }; - - if (esClientConfig.log == null) { - esClientConfig.log = getLoggerClass(log, type); - } - - if (config.pingTimeout != null) { - esClientConfig.pingTimeout = getDurationAsMs(config.pingTimeout); - } - - if (config.requestTimeout != null) { - esClientConfig.requestTimeout = getDurationAsMs(config.requestTimeout); - } - - if (config.sniffInterval) { - esClientConfig.sniffInterval = getDurationAsMs(config.sniffInterval); - } - - const needsAuth = - auth !== false && ((config.username && config.password) || config.serviceAccountToken); - if (needsAuth) { - if (config.username) { - esClientConfig.httpAuth = `${config.username}:${config.password}`; - } else if (config.serviceAccountToken) { - esClientConfig.serviceAccountToken = config.serviceAccountToken; - } - } - - if (Array.isArray(config.hosts)) { - esClientConfig.hosts = config.hosts.map((nodeUrl: string) => { - const uri = url.parse(nodeUrl); - const httpsURI = uri.protocol === 'https:'; - const httpURI = uri.protocol === 'http:'; - - const host: Record = { - host: uri.hostname, - port: uri.port || (httpsURI && '443') || (httpURI && '80'), - protocol: uri.protocol, - path: uri.pathname, - query: uri.query, - headers: { - ...DEFAULT_HEADERS, - ...config.customHeaders, - }, - }; - - return host; - }); - } - - if (config.ssl === undefined) { - return cloneDeep(esClientConfig); - } - - esClientConfig.ssl = {}; - - const verificationMode = config.ssl.verificationMode; - switch (verificationMode) { - case 'none': - esClientConfig.ssl.rejectUnauthorized = false; - break; - case 'certificate': - esClientConfig.ssl.rejectUnauthorized = true; - - // by default, NodeJS is checking the server identify - esClientConfig.ssl.checkServerIdentity = () => undefined; - break; - case 'full': - esClientConfig.ssl.rejectUnauthorized = true; - break; - default: - throw new Error(`Unknown ssl verificationMode: ${verificationMode}`); - } - - esClientConfig.ssl.ca = config.ssl.certificateAuthorities; - - // Add client certificate and key if required by elasticsearch - if (!ignoreCertAndKey && config.ssl.certificate && config.ssl.key) { - esClientConfig.ssl.cert = config.ssl.certificate; - esClientConfig.ssl.key = config.ssl.key; - esClientConfig.ssl.passphrase = config.ssl.keyPassphrase; - } - - // Elasticsearch JS client mutates config object, so all properties that are - // usually passed by reference should be cloned to avoid any side effects. - return cloneDeep(esClientConfig); -} - -function getDurationAsMs(duration: number | Duration) { - if (typeof duration === 'number') { - return duration; - } - - return duration.asMilliseconds(); -} - -function getLoggerClass(log: Logger, type: string) { - const queryLogger = log.get('query', type); - - return class ElasticsearchClientLogging { - public error(err: string | Error) { - log.error(err); - } - - public warning(message: string) { - log.warn(message); - } - - public trace( - method: string, - options: { path: string }, - query: string, - _: unknown, - statusCode: string - ) { - queryLogger.debug(`${statusCode}\n${method} ${options.path}\n${query ? query.trim() : ''}`); - } - - // elasticsearch-js expects the following functions to exist - public info() { - // noop - } - - public debug() { - // noop - } - - public close() { - // noop - } - }; -} diff --git a/src/core/server/elasticsearch/legacy/errors.test.ts b/src/core/server/elasticsearch/legacy/errors.test.ts deleted file mode 100644 index 9973f70967a50..0000000000000 --- a/src/core/server/elasticsearch/legacy/errors.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Boom from '@hapi/boom'; - -import { LegacyElasticsearchErrorHelpers } from './errors'; - -describe('ElasticsearchErrorHelpers', () => { - describe('NotAuthorized error', () => { - describe('decorateNotAuthorizedError', () => { - it('returns original object', () => { - const error = new Error(); - expect(LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(error)).toBe(error); - }); - - it('makes the error identifiable as a NotAuthorized error', () => { - const error = new Error(); - expect(LegacyElasticsearchErrorHelpers.isNotAuthorizedError(error)).toBe(false); - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(error); - expect(LegacyElasticsearchErrorHelpers.isNotAuthorizedError(error)).toBe(true); - }); - - it('adds boom properties', () => { - const error = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - expect(typeof error.output).toBe('object'); - expect(error.output.statusCode).toBe(401); - }); - - it('preserves boom properties of input', () => { - const error = Boom.notFound(); - LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(error); - expect(error.output.statusCode).toBe(404); - }); - - describe('error.output', () => { - it('defaults to message of error', () => { - const error = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError( - new Error('foobar') - ); - expect(error.output.payload).toHaveProperty('message', 'foobar'); - }); - it('prefixes message with passed reason', () => { - const error = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError( - new Error('foobar'), - 'biz' - ); - expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); - }); - it('sets statusCode to 401', () => { - const error = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError( - new Error('foo') - ); - expect(error.output).toHaveProperty('statusCode', 401); - }); - }); - }); - }); -}); diff --git a/src/core/server/elasticsearch/legacy/errors.ts b/src/core/server/elasticsearch/legacy/errors.ts deleted file mode 100644 index 4111661bb83c4..0000000000000 --- a/src/core/server/elasticsearch/legacy/errors.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Boom from '@hapi/boom'; -import { get } from 'lodash'; - -const code = Symbol('ElasticsearchError'); - -enum ErrorCode { - NOT_AUTHORIZED = 'Elasticsearch/notAuthorized', -} - -/** - * @deprecated. The new elasticsearch client doesn't wrap errors anymore. - * @removeBy 7.16 - * @public - * */ -export interface LegacyElasticsearchError extends Boom.Boom { - [code]?: string; -} - -function isElasticsearchError(error: any): error is LegacyElasticsearchError { - return Boolean(error && error[code]); -} - -function decorate( - error: Error, - errorCode: ErrorCode, - statusCode: number, - message?: string -): LegacyElasticsearchError { - if (isElasticsearchError(error)) { - return error; - } - - const boom = Boom.boomify(error, { - statusCode, - message, - // keep status and messages if Boom error object already has them - override: false, - }) as LegacyElasticsearchError; - - boom[code] = errorCode; - - return boom; -} - -/** - * Helpers for working with errors returned from the Elasticsearch service.Since the internal data of - * errors are subject to change, consumers of the Elasticsearch service should always use these helpers - * to classify errors instead of checking error internals such as `body.error.header[WWW-Authenticate]` - * @public - * - * @example - * Handle errors - * ```js - * try { - * await client.asScoped(request).callAsCurrentUser(...); - * } catch (err) { - * if (ElasticsearchErrorHelpers.isNotAuthorizedError(err)) { - * const authHeader = err.output.headers['WWW-Authenticate']; - * } - * ``` - */ -export class LegacyElasticsearchErrorHelpers { - public static isNotAuthorizedError(error: any): error is LegacyElasticsearchError { - return isElasticsearchError(error) && error[code] === ErrorCode.NOT_AUTHORIZED; - } - - public static decorateNotAuthorizedError(error: Error, reason?: string) { - const decoratedError = decorate(error, ErrorCode.NOT_AUTHORIZED, 401, reason); - const wwwAuthHeader = get(error, 'body.error.header[WWW-Authenticate]') as string; - - (decoratedError.output.headers as { [key: string]: string })['WWW-Authenticate'] = - wwwAuthHeader || 'Basic realm="Authorization Required"'; - - return decoratedError; - } -} diff --git a/src/core/server/elasticsearch/legacy/index.ts b/src/core/server/elasticsearch/legacy/index.ts deleted file mode 100644 index d98b7f16635a1..0000000000000 --- a/src/core/server/elasticsearch/legacy/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { LegacyClusterClient } from './cluster_client'; -export type { ILegacyClusterClient, ILegacyCustomClusterClient } from './cluster_client'; -export type { - ILegacyScopedClusterClient, - LegacyScopedClusterClient, -} from './scoped_cluster_client'; -export type { LegacyElasticsearchClientConfig } from './elasticsearch_client_config'; -export { LegacyElasticsearchErrorHelpers } from './errors'; -export type { LegacyElasticsearchError } from './errors'; -export * from './api_types'; diff --git a/src/core/server/elasticsearch/legacy/mocks.ts b/src/core/server/elasticsearch/legacy/mocks.ts deleted file mode 100644 index c8787af6bdb34..0000000000000 --- a/src/core/server/elasticsearch/legacy/mocks.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Client } from 'elasticsearch'; -import { ILegacyScopedClusterClient } from './scoped_cluster_client'; -import { ILegacyClusterClient, ILegacyCustomClusterClient } from './cluster_client'; - -const createScopedClusterClientMock = (): jest.Mocked => ({ - callAsInternalUser: jest.fn(), - callAsCurrentUser: jest.fn(), -}); - -const createCustomClusterClientMock = (): jest.Mocked => ({ - ...createClusterClientMock(), - close: jest.fn(), -}); - -function createClusterClientMock() { - const client: jest.Mocked = { - callAsInternalUser: jest.fn(), - asScoped: jest.fn(), - }; - client.asScoped.mockReturnValue(createScopedClusterClientMock()); - return client; -} - -const createElasticsearchClientMock = () => { - const mocked: jest.Mocked = { - cat: {} as any, - cluster: {} as any, - indices: {} as any, - ingest: {} as any, - nodes: {} as any, - snapshot: {} as any, - tasks: {} as any, - bulk: jest.fn(), - clearScroll: jest.fn(), - count: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - deleteByQuery: jest.fn(), - deleteScript: jest.fn(), - deleteTemplate: jest.fn(), - exists: jest.fn(), - explain: jest.fn(), - fieldStats: jest.fn(), - get: jest.fn(), - getScript: jest.fn(), - getSource: jest.fn(), - getTemplate: jest.fn(), - index: jest.fn(), - info: jest.fn(), - mget: jest.fn(), - msearch: jest.fn(), - msearchTemplate: jest.fn(), - mtermvectors: jest.fn(), - ping: jest.fn(), - putScript: jest.fn(), - putTemplate: jest.fn(), - reindex: jest.fn(), - reindexRethrottle: jest.fn(), - renderSearchTemplate: jest.fn(), - scroll: jest.fn(), - search: jest.fn(), - searchShards: jest.fn(), - searchTemplate: jest.fn(), - suggest: jest.fn(), - termvectors: jest.fn(), - update: jest.fn(), - updateByQuery: jest.fn(), - close: jest.fn(), - }; - return mocked; -}; - -export const legacyClientMock = { - createScopedClusterClient: createScopedClusterClientMock, - createCustomClusterClient: createCustomClusterClientMock, - createClusterClient: createClusterClientMock, - createElasticsearchClient: createElasticsearchClientMock, -}; diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts deleted file mode 100644 index 6607f3b694d82..0000000000000 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LegacyScopedClusterClient } from './scoped_cluster_client'; - -let internalAPICaller: jest.Mock; -let scopedAPICaller: jest.Mock; -let clusterClient: LegacyScopedClusterClient; -beforeEach(() => { - internalAPICaller = jest.fn(); - scopedAPICaller = jest.fn(); - clusterClient = new LegacyScopedClusterClient(internalAPICaller, scopedAPICaller, { one: '1' }); -}); - -afterEach(() => jest.clearAllMocks()); - -describe('#callAsInternalUser', () => { - test('properly forwards arguments to the API caller and results back from it', async () => { - const mockResponse = { data: 'response' }; - internalAPICaller.mockResolvedValue(mockResponse); - - await expect(clusterClient.callAsInternalUser('ping')).resolves.toBe(mockResponse); - expect(internalAPICaller).toHaveBeenCalledTimes(1); - expect(internalAPICaller).toHaveBeenCalledWith('ping', {}, undefined); - internalAPICaller.mockClear(); - - await expect( - clusterClient.callAsInternalUser('security.authenticate', { some: 'some' }) - ).resolves.toBe(mockResponse); - expect(internalAPICaller).toHaveBeenCalledTimes(1); - expect(internalAPICaller).toHaveBeenCalledWith( - 'security.authenticate', - { some: 'some' }, - undefined - ); - internalAPICaller.mockClear(); - - await expect( - clusterClient.callAsInternalUser('ping', undefined, { wrap401Errors: true }) - ).resolves.toBe(mockResponse); - expect(internalAPICaller).toHaveBeenCalledTimes(1); - expect(internalAPICaller).toHaveBeenCalledWith('ping', {}, { wrap401Errors: true }); - internalAPICaller.mockClear(); - - await expect( - clusterClient.callAsInternalUser( - 'security.authenticate', - { some: 'some' }, - { wrap401Errors: true } - ) - ).resolves.toBe(mockResponse); - expect(internalAPICaller).toHaveBeenCalledTimes(1); - expect(internalAPICaller).toHaveBeenCalledWith( - 'security.authenticate', - { some: 'some' }, - { wrap401Errors: true } - ); - - expect(scopedAPICaller).not.toHaveBeenCalled(); - }); - - test('properly forwards errors returned by the API caller', async () => { - const mockErrorResponse = new Error('some-error'); - internalAPICaller.mockRejectedValue(mockErrorResponse); - - await expect(clusterClient.callAsInternalUser('ping')).rejects.toBe(mockErrorResponse); - - expect(scopedAPICaller).not.toHaveBeenCalled(); - }); -}); - -describe('#callAsCurrentUser', () => { - test('properly forwards arguments to the API caller and results back from it', async () => { - const mockResponse = { data: 'response' }; - scopedAPICaller.mockResolvedValue(mockResponse); - - await expect(clusterClient.callAsCurrentUser('ping')).resolves.toBe(mockResponse); - expect(scopedAPICaller).toHaveBeenCalledTimes(1); - expect(scopedAPICaller).toHaveBeenCalledWith('ping', { headers: { one: '1' } }, undefined); - scopedAPICaller.mockClear(); - - await expect( - clusterClient.callAsCurrentUser('security.authenticate', { some: 'some' }) - ).resolves.toBe(mockResponse); - expect(scopedAPICaller).toHaveBeenCalledTimes(1); - expect(scopedAPICaller).toHaveBeenCalledWith( - 'security.authenticate', - { some: 'some', headers: { one: '1' } }, - undefined - ); - scopedAPICaller.mockClear(); - - await expect( - clusterClient.callAsCurrentUser('security.authenticate', { some: 'some' }) - ).resolves.toBe(mockResponse); - expect(scopedAPICaller).toHaveBeenCalledTimes(1); - expect(scopedAPICaller).toHaveBeenCalledWith( - 'security.authenticate', - { some: 'some', headers: { one: '1' } }, - undefined - ); - scopedAPICaller.mockClear(); - - await expect( - clusterClient.callAsCurrentUser('ping', undefined, { wrap401Errors: true }) - ).resolves.toBe(mockResponse); - expect(scopedAPICaller).toHaveBeenCalledTimes(1); - expect(scopedAPICaller).toHaveBeenCalledWith( - 'ping', - { headers: { one: '1' } }, - { wrap401Errors: true } - ); - scopedAPICaller.mockClear(); - - await expect( - clusterClient.callAsCurrentUser( - 'security.authenticate', - { some: 'some' }, - { wrap401Errors: true } - ) - ).resolves.toBe(mockResponse); - - expect(scopedAPICaller).toHaveBeenCalledTimes(1); - expect(scopedAPICaller).toHaveBeenCalledWith( - 'security.authenticate', - { some: 'some', headers: { one: '1' } }, - { wrap401Errors: true } - ); - - expect(internalAPICaller).not.toHaveBeenCalled(); - }); - - test('callAsCurrentUser allows passing additional headers', async () => { - const mockResponse = { data: 'response' }; - scopedAPICaller.mockResolvedValue(mockResponse); - await expect( - clusterClient.callAsCurrentUser('security.authenticate', { - some: 'some', - headers: { additionalHeader: 'Oh Yes!' }, - }) - ).resolves.toBe(mockResponse); - expect(scopedAPICaller).toHaveBeenCalledTimes(1); - expect(scopedAPICaller).toHaveBeenCalledWith( - 'security.authenticate', - { some: 'some', headers: { one: '1', additionalHeader: 'Oh Yes!' } }, - undefined - ); - }); - - test('callAsCurrentUser cannot override default headers', async () => { - const expectedErrorResponse = new Error('Cannot override default header one.'); - const withHeaderOverride = async () => - clusterClient.callAsCurrentUser('security.authenticate', { headers: { one: 'OVERRIDE' } }); - await expect(withHeaderOverride()).rejects.toThrowError(expectedErrorResponse); - expect(scopedAPICaller).toHaveBeenCalledTimes(0); - }); - - test('properly forwards errors returned by the API caller', async () => { - const mockErrorResponse = new Error('some-error'); - scopedAPICaller.mockRejectedValue(mockErrorResponse); - - await expect(clusterClient.callAsCurrentUser('ping')).rejects.toBe(mockErrorResponse); - - expect(internalAPICaller).not.toHaveBeenCalled(); - }); - - test('does not attach headers to the client params if they are not available', async () => { - const mockResponse = { data: 'response' }; - scopedAPICaller.mockResolvedValue(mockResponse); - - const clusterClientWithoutHeaders = new LegacyScopedClusterClient( - internalAPICaller, - scopedAPICaller - ); - - await expect(clusterClientWithoutHeaders.callAsCurrentUser('ping')).resolves.toBe(mockResponse); - expect(scopedAPICaller).toHaveBeenCalledTimes(1); - expect(scopedAPICaller).toHaveBeenCalledWith('ping', {}, undefined); - - scopedAPICaller.mockClear(); - await expect( - clusterClientWithoutHeaders.callAsCurrentUser('security.authenticate', { some: 'some' }) - ).resolves.toBe(mockResponse); - expect(scopedAPICaller).toHaveBeenCalledTimes(1); - expect(scopedAPICaller).toHaveBeenCalledWith( - 'security.authenticate', - { some: 'some' }, - undefined - ); - - expect(internalAPICaller).not.toHaveBeenCalled(); - }); -}); diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts deleted file mode 100644 index d58519dd25a7f..0000000000000 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { intersection, isObject } from 'lodash'; -import { Headers } from '../../http/router'; -import { LegacyAPICaller, LegacyCallAPIOptions } from './api_types'; - -/** - * Serves the same purpose as "normal" `ClusterClient` but exposes additional - * `callAsCurrentUser` method that doesn't use credentials of the Kibana internal - * user (as `callAsInternalUser` does) to request Elasticsearch API, but rather - * passes HTTP headers extracted from the current user request to the API. - * - * See {@link LegacyScopedClusterClient}. - * - * @deprecated Use {@link IScopedClusterClient}. - * @removeBy 7.16 - * @public - */ -export type ILegacyScopedClusterClient = Pick< - LegacyScopedClusterClient, - 'callAsCurrentUser' | 'callAsInternalUser' ->; - -/** - * {@inheritDoc IScopedClusterClient} - * @deprecated Use {@link IScopedClusterClient | scoped cluster client}. - * @removeBy 7.16 - * @public - */ -export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { - constructor( - private readonly internalAPICaller: LegacyAPICaller, - private readonly scopedAPICaller: LegacyAPICaller, - private readonly headers?: Headers - ) { - this.callAsCurrentUser = this.callAsCurrentUser.bind(this); - this.callAsInternalUser = this.callAsInternalUser.bind(this); - } - - /** - * Calls specified `endpoint` with provided `clientParams` on behalf of the - * Kibana internal user. - * See {@link LegacyAPICaller}. - * @deprecated Use {@link IScopedClusterClient.asInternalUser}. - * @removeBy 7.16 - * - * @param endpoint - String descriptor of the endpoint e.g. `cluster.getSettings` or `ping`. - * @param clientParams - A dictionary of parameters that will be passed directly to the Elasticsearch JS client. - * @param options - Options that affect the way we call the API and process the result. - */ - public callAsInternalUser( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) { - return this.internalAPICaller(endpoint, clientParams, options); - } - - /** - * Calls specified `endpoint` with provided `clientParams` on behalf of the - * user initiated request to the Kibana server (via HTTP request headers). - * See {@link LegacyAPICaller}. - * @deprecated Use {@link IScopedClusterClient.asCurrentUser}. - * @removeBy 7.16 - * - * @param endpoint - String descriptor of the endpoint e.g. `cluster.getSettings` or `ping`. - * @param clientParams - A dictionary of parameters that will be passed directly to the Elasticsearch JS client. - * @param options - Options that affect the way we call the API and process the result. - */ - public callAsCurrentUser( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) { - const defaultHeaders = this.headers; - if (defaultHeaders !== undefined) { - const customHeaders: any = clientParams.headers; - if (isObject(customHeaders)) { - const duplicates = intersection(Object.keys(defaultHeaders), Object.keys(customHeaders)); - duplicates.forEach((duplicate) => { - if (defaultHeaders[duplicate] !== (customHeaders as any)[duplicate]) { - throw Error(`Cannot override default header ${duplicate}.`); - } - }); - } - - clientParams.headers = Object.assign({}, clientParams.headers, this.headers); - } - - return this.scopedAPICaller(endpoint, clientParams, options); - } -} diff --git a/src/core/server/elasticsearch/supported_server_response_check.test.ts b/src/core/server/elasticsearch/supported_server_response_check.test.ts new file mode 100644 index 0000000000000..589e947142fc3 --- /dev/null +++ b/src/core/server/elasticsearch/supported_server_response_check.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isNotFoundFromUnsupportedServer } from './supported_server_response_check'; + +describe('#isNotFoundFromUnsupportedServer', () => { + it('returns true with not found response from unsupported server', () => { + const rawResponse = { + statusCode: 404, + headers: {}, + }; + + const result = isNotFoundFromUnsupportedServer(rawResponse); + expect(result).toBe(true); + }); + + it('returns false with not found response from supported server', async () => { + const rawResponse = { + statusCode: 404, + headers: { 'x-elastic-product': 'Elasticsearch' }, + }; + + const result = isNotFoundFromUnsupportedServer(rawResponse); + expect(result).toBe(false); + }); + + it('returns false when not a 404', async () => { + const rawResponse = { + statusCode: 200, + headers: { 'x-elastic-product': 'Elasticsearch' }, + }; + + const result = isNotFoundFromUnsupportedServer(rawResponse); + expect(result).toBe(false); + }); +}); diff --git a/src/core/server/elasticsearch/supported_server_response_check.ts b/src/core/server/elasticsearch/supported_server_response_check.ts index 6fe812bc58518..85235d04caf5c 100644 --- a/src/core/server/elasticsearch/supported_server_response_check.ts +++ b/src/core/server/elasticsearch/supported_server_response_check.ts @@ -12,6 +12,22 @@ export const PRODUCT_RESPONSE_HEADER = 'x-elastic-product'; * @returns boolean */ // This check belongs to the elasticsearch service as a dedicated helper method. -export const isSupportedEsServer = (headers: Record | null) => { +export const isSupportedEsServer = (headers: Record | null) => { return !!headers && headers[PRODUCT_RESPONSE_HEADER] === 'Elasticsearch'; }; + +/** + * Check to ensure that a 404 response does not come from Elasticsearch + * + * WARNING: This is a hack to work around for 404 responses returned from a proxy. + * We're aiming to minimise the risk of data loss when consumers act on Not Found errors + * + * @param response response from elasticsearch client call + * @returns boolean 'true' if the status code is 404 and the Elasticsearch product header is missing/unexpected value + */ +export const isNotFoundFromUnsupportedServer = (args: { + statusCode: number | null; + headers: Record | null; +}): boolean => { + return args.statusCode === 404 && !isSupportedEsServer(args.headers); +}; diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 375c7015b16d7..89a7d752f7912 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -10,11 +10,6 @@ import { Observable } from 'rxjs'; import { Headers } from '../http/router'; import { LegacyRequest, KibanaRequest } from '../http'; import { ElasticsearchConfig } from './elasticsearch_config'; -import { - LegacyElasticsearchClientConfig, - ILegacyClusterClient, - ILegacyCustomClusterClient, -} from './legacy'; import { IClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from './client'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus } from '../status'; @@ -71,46 +66,6 @@ export interface ElasticsearchServiceSetup { * @deprecated this will be removed in a later version. */ readonly config$: Observable; - /** - * @deprecated - * @removeBy 7.16 - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.createClient} instead. - * - * Create application specific Elasticsearch cluster API client with customized config. See {@link ILegacyClusterClient}. - * - * @param type Unique identifier of the client - * @param clientConfig A config consists of Elasticsearch JS client options and - * valid sub-set of Elasticsearch service config. - * We fill all the missing properties in the `clientConfig` using the default - * Elasticsearch config so that we don't depend on default values set and - * controlled by underlying Elasticsearch JS client. - * We don't run validation against the passed config and expect it to be valid. - * - * @example - * ```js - * const client = elasticsearch.createCluster('my-app-name', config); - * const data = await client.callAsInternalUser(); - * ``` - */ - readonly createClient: ( - type: string, - clientConfig?: Partial - ) => ILegacyCustomClusterClient; - - /** - * @removeBy 7.16 - * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. - * - * All Elasticsearch config value changes are processed under the hood. - * See {@link ILegacyClusterClient}. - * - * @example - * ```js - * const client = core.elasticsearch.legacy.client; - * ``` - */ - readonly client: ILegacyClusterClient; }; } @@ -170,43 +125,6 @@ export interface ElasticsearchServiceStart { * @deprecated this will be removed in a later version. */ readonly config$: Observable; - /** - * Create application specific Elasticsearch cluster API client with customized config. See {@link ILegacyClusterClient}. - * - * @deprecated - * @removeBy 7.16 - * - * @param type Unique identifier of the client - * @param clientConfig A config consists of Elasticsearch JS client options and - * valid sub-set of Elasticsearch service config. - * We fill all the missing properties in the `clientConfig` using the default - * Elasticsearch config so that we don't depend on default values set and - * controlled by underlying Elasticsearch JS client. - * We don't run validation against the passed config and expect it to be valid. - * - * @example - * ```js - * const client = elasticsearch.legacy.createClient('my-app-name', config); - * const data = await client.callAsInternalUser(); - * ``` - */ - readonly createClient: ( - type: string, - clientConfig?: Partial - ) => ILegacyCustomClusterClient; - - /** - * A pre-configured {@link ILegacyClusterClient | legacy Elasticsearch client}. - * - * @deprecated - * @removeBy 7.16 - * - * @example - * ```js - * const client = core.elasticsearch.legacy.client; - * ``` - */ - readonly client: ILegacyClusterClient; }; } diff --git a/src/core/server/http/integration_tests/core_service.test.mocks.ts b/src/core/server/http/integration_tests/core_service.test.mocks.ts index 2e2d8a4b5f03a..9aa801c1e7759 100644 --- a/src/core/server/http/integration_tests/core_service.test.mocks.ts +++ b/src/core/server/http/integration_tests/core_service.test.mocks.ts @@ -6,27 +6,6 @@ * Side Public License, v 1. */ -import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock'; - -export const MockLegacyScopedClusterClient = jest.fn(); -export const legacyClusterClientInstanceMock = elasticsearchServiceMock.createLegacyScopedClusterClient(); -jest.doMock('../../elasticsearch/legacy/scoped_cluster_client', () => ({ - LegacyScopedClusterClient: MockLegacyScopedClusterClient.mockImplementation( - () => legacyClusterClientInstanceMock - ), -})); - -jest.doMock('elasticsearch', () => { - const realES = jest.requireActual('elasticsearch'); - return { - ...realES, - // eslint-disable-next-line object-shorthand - Client: function () { - return elasticsearchServiceMock.createLegacyElasticsearchClient(); - }, - }; -}); - export const MockElasticsearchClient = jest.fn(); jest.doMock('@elastic/elasticsearch', () => { const real = jest.requireActual('@elastic/elasticsearch'); diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index e497f254e0632..0c2d6896573bd 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -6,15 +6,7 @@ * Side Public License, v 1. */ -import { - MockLegacyScopedClusterClient, - MockElasticsearchClient, - legacyClusterClientInstanceMock, -} from './core_service.test.mocks'; - -import { errors as esErrors } from 'elasticsearch'; -import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy'; - +import { MockElasticsearchClient } from './core_service.test.mocks'; import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import * as kbnTestServer from '../../../test_helpers/kbn_server'; @@ -186,99 +178,6 @@ describe('http service', () => { }); }); - describe('legacy elasticsearch client', () => { - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot({ plugins: { initialize: false } }); - await root.preboot(); - }, 30000); - - afterEach(async () => { - MockLegacyScopedClusterClient.mockClear(); - await root.shutdown(); - }); - - it('rewrites authorization header via authHeaders to make a request to Elasticsearch', async () => { - const authHeaders = { authorization: 'Basic: user:password' }; - const { http } = await root.setup(); - const { registerAuth, createRouter } = http; - - registerAuth((req, res, toolkit) => toolkit.authenticated({ requestHeaders: authHeaders })); - - const router = createRouter('/new-platform'); - router.get({ path: '/', validate: false }, async (context, req, res) => { - // it forces client initialization since the core creates them lazily. - await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping'); - return res.ok(); - }); - - await root.start(); - - await kbnTestServer.request.get(root, '/new-platform/').expect(200); - - // client contains authHeaders for BWC with legacy platform. - const [client] = MockLegacyScopedClusterClient.mock.calls; - const [, , clientHeaders] = client; - expect(clientHeaders).toEqual({ - ...authHeaders, - 'x-opaque-id': expect.any(String), - }); - }); - - it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => { - const authorizationHeader = 'Basic: username:password'; - const { http } = await root.setup(); - const { createRouter } = http; - - const router = createRouter('/new-platform'); - router.get({ path: '/', validate: false }, async (context, req, res) => { - // it forces client initialization since the core creates them lazily. - await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping'); - return res.ok(); - }); - - await root.start(); - - await kbnTestServer.request - .get(root, '/new-platform/') - .set('Authorization', authorizationHeader) - .expect(200); - - const [client] = MockLegacyScopedClusterClient.mock.calls; - const [, , clientHeaders] = client; - expect(clientHeaders).toEqual({ - authorization: authorizationHeader, - 'x-opaque-id': expect.any(String), - }); - }); - - it('forwards 401 errors returned from elasticsearch', async () => { - const { http } = await root.setup(); - const { createRouter } = http; - - const authenticationError = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (esErrors.AuthenticationException as any)('Authentication Exception', { - body: { error: { header: { 'WWW-Authenticate': 'authenticate header' } } }, - statusCode: 401, - }) - ); - - legacyClusterClientInstanceMock.callAsCurrentUser.mockRejectedValue(authenticationError); - - const router = createRouter('/new-platform'); - router.get({ path: '/', validate: false }, async (context, req, res) => { - await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping'); - return res.ok(); - }); - - await root.start(); - - const response = await kbnTestServer.request.get(root, '/new-platform/').expect(401); - - expect(response.header['www-authenticate']).toEqual('authenticate header'); - }); - }); - describe('elasticsearch client', () => { let root: ReturnType; diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 1e7297ddcba3b..75fcc1bb0e083 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -11,7 +11,6 @@ import Boom from '@hapi/boom'; import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../../logging'; -import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy/errors'; import { isUnauthorizedError as isElasticsearchUnauthorizedError, UnauthorizedError as EsNotAuthorizedError, @@ -280,10 +279,6 @@ export class Router { // obj3 is excluded from the results ]); }); + it(`handles 404 responses that don't come from Elasticsearch`, async () => { + const createEsUnavailableNotFoundError = () => { + return SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + }; + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { docs: [] }, + { statusCode: 404 }, + {} + ) + ); + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrowError( + createEsUnavailableNotFoundError() + ); + }); describe('legacy URL aliases', () => { it('uses the PointInTimeFinder to search for legacy URL aliases', async () => { diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts index b82dfec9467c3..7acbaaea1f5d7 100644 --- a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts @@ -7,11 +7,12 @@ */ import * as esKuery from '@kbn/es-query'; - +import { isNotFoundFromUnsupportedServer } from '../../../elasticsearch'; import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import type { SavedObjectsSerializer } from '../../serialization'; import type { SavedObject, SavedObjectsBaseOptions } from '../../types'; +import { SavedObjectsErrorHelpers } from './errors'; import { getRootFields } from './included_fields'; import { getSavedObjectFromSource, rawDocExistsInNamespace } from './internal_utils'; import type { @@ -198,6 +199,15 @@ async function getObjectsAndReferences({ { body: { docs: makeBulkGetDocs(bulkGetObjects) } }, { ignore: [404] } ); + // exit early if we can't verify a 404 response is from Elasticsearch + if ( + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } const newObjectsToGet = new Set(); for (let i = 0; i < bulkGetObjects.length; i++) { // For every element in bulkGetObjects, there should be a matching element in bulkGetResponse.body.docs diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index c1e1e9589b9ae..7412e744f19e7 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -203,7 +203,11 @@ export class SavedObjectsErrorHelpers { return isSavedObjectsClientError(error) && error[code] === CODE_GENERAL_ERROR; } - public static createGenericNotFoundEsUnavailableError(type: string, id: string) { + public static createGenericNotFoundEsUnavailableError( + // type and id not available in all operations (e.g. mget) + type: string | null = null, + id: string | null = null + ) { const notFoundError = this.createGenericNotFoundError(type, id); return this.decorateEsUnavailableError( new Error(`${notFoundError.message}`), diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index eead42db1ec58..efae5bd737020 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -652,6 +652,29 @@ describe('SavedObjectsRepository', () => { }); }; + const unsupportedProductBulkCreateMgetError = async (objects, options) => { + const multiNamespaceObjects = objects.filter( + ({ type, id }) => registry.isMultiNamespace(type) && id + ); + if (multiNamespaceObjects?.length) { + const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); + client.mget.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...response }, + { statusCode: 404 }, + {} + ) + ); + } + const response = getMockBulkCreateResponse(objects, options?.namespace); + client.bulk.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + await expect(savedObjectsRepository.bulkCreate(objects, options)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError() + ); + }; + it(`throws when options.namespace is '*'`, async () => { await expect( savedObjectsRepository.bulkCreate([obj3], { namespace: ALL_NAMESPACES_STRING }) @@ -759,6 +782,13 @@ describe('SavedObjectsRepository', () => { const expectedErrorResult = { type: obj3.type, id: obj3.id, error: 'Oh no, a bulk error!' }; await bulkCreateError(obj3, true, expectedErrorResult); }); + + it(`throws when ES mget action returns 404 with missing Elasticsearch header`, async () => { + const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }]; + await unsupportedProductBulkCreateMgetError(objects); + expect(client.mget).toHaveBeenCalledTimes(1); + expect(client.bulk).toHaveBeenCalledTimes(0); + }); }); describe('migration', () => { @@ -1011,6 +1041,21 @@ describe('SavedObjectsRepository', () => { }); }; + const unsupportedProductBulkGetMgetError = async (objects, options) => { + const response = getMockMgetResponse(objects, options?.namespace); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...response }, + { statusCode: 404 }, + {} + ) + ); + await expect(bulkGet(objects, options)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError() + ); + expect(client.mget).toHaveBeenCalledTimes(1); + }; + it(`throws when options.namespace is '*'`, async () => { const obj = { type: 'dashboard', id: 'three' }; await expect( @@ -1046,6 +1091,12 @@ describe('SavedObjectsRepository', () => { response.docs[1].namespaces = ['bar-namespace']; await bulkGetErrorNotFound([obj1, obj, obj2], { namespace }, response); }); + + it(`throws when ES mget action responds with a 404 and a missing Elasticsearch product header`, async () => { + const getId = (type, id) => `${type}:${id}`; + await unsupportedProductBulkGetMgetError([obj1, obj2]); // returns 404 without required product header + _expectClientCallArgs([obj1, obj2], { getId }); + }); }); describe('returns', () => { @@ -1450,6 +1501,34 @@ describe('SavedObjectsRepository', () => { saved_objects: [expectSuccess(obj1), expectErrorNotFound(_obj), expectSuccess(obj2)], }); }; + const unsupportedProductBulkUpdateMgetError = async (objects, options, includeOriginId) => { + const multiNamespaceObjects = objects.filter(({ type }) => registry.isMultiNamespace(type)); + if (multiNamespaceObjects?.length) { + const response = getMockMgetResponse(multiNamespaceObjects, options?.namespace); + client.mget.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...response }, + { statusCode: 404 }, + {} + ) + ); + } + const response = getMockBulkUpdateResponse(objects, options?.namespace, includeOriginId); + client.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + + await expect(savedObjectsRepository.bulkUpdate(objects, options)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError() + ); + expect(client.mget).toHaveBeenCalledTimes(multiNamespaceObjects?.length ? 1 : 0); + }; + + it(`throws when ES mget action responds with a 404 and a missing Elasticsearch product header`, async () => { + const objects = [obj1, { ...obj2, type: MULTI_NAMESPACE_ISOLATED_TYPE }]; + await unsupportedProductBulkUpdateMgetError(objects); + expect(client.mget).toHaveBeenCalledTimes(1); + }); it(`throws when options.namespace is '*'`, async () => { await expect( @@ -1651,6 +1730,24 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository.checkConflicts([obj1], { namespace: ALL_NAMESPACES_STRING }) ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); }); + + it(`throws when not found responses aren't from Elasticsearch`, async () => { + const checkConflictsMgetError = async (objects, options) => { + const response = getMockMgetResponse(objects, options?.namespace); + client.mget.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...response }, + { statusCode: 404 }, + {} + ) + ); + await expect(checkConflicts(objects, options)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError() + ); + expect(client.mget).toHaveBeenCalledTimes(1); + }; + await checkConflictsMgetError([obj1, obj2], { namespace: 'default' }); + }); }); describe('returns', () => { @@ -2228,11 +2325,7 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - { found: false }, - undefined, - { 'x-elastic-product': 'Elasticsearch' } - ) + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }, undefined) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -2240,11 +2333,7 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - {}, - { statusCode: 404 }, - { 'x-elastic-product': 'Elasticsearch' } - ) + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -2252,14 +2341,18 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get with missing Elasticsearch header`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + { statusCode: 404 }, + {} + ) ); await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); }); it(`throws when ES is unable to find the index during get with missing Elasticsearch header`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }, {}) ); await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); }); @@ -2305,7 +2398,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during delete`, async () => { client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { result: 'not_found' }, + {}, + {} + ) ); await expectNotFoundEsUnavailableError(type, id); expect(client.delete).toHaveBeenCalledTimes(1); @@ -2313,9 +2410,13 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during delete`, async () => { client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - error: { type: 'index_not_found_exception' }, - }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { + error: { type: 'index_not_found_exception' }, + }, + {}, + {} + ) ); await expectNotFoundEsUnavailableError(type, id); expect(client.delete).toHaveBeenCalledTimes(1); @@ -2572,6 +2673,22 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository.removeReferencesTo(type, id, defaultOptions) ).rejects.toThrowError(createConflictError(type, id)); }); + + it(`throws on 404 with missing Elasticsearch header`, async () => { + client.updateByQuery.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { + updated: updatedCount, + }, + { statusCode: 404 }, + {} + ) + ); + await expect( + savedObjectsRepository.removeReferencesTo(type, id, defaultOptions) + ).rejects.toThrowError(createGenericNotFoundEsUnavailableError(type, id)); + expect(client.updateByQuery).toHaveBeenCalledTimes(1); + }); }); }); @@ -2748,6 +2865,21 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { + const findNotSupportedServerError = async (options, namespace) => { + const expectedSearchResults = generateSearchResults(namespace); + client.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...expectedSearchResults }, + { statusCode: 404 }, + {} + ) + ); + await expect(savedObjectsRepository.find(options)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError() + ); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); + expect(client.search).toHaveBeenCalledTimes(1); + }; it(`throws when type is not defined`, async () => { await expect(savedObjectsRepository.find({})).rejects.toThrowError( 'options.type must be a string or an array of strings' @@ -2828,6 +2960,11 @@ describe('SavedObjectsRepository', () => { expect(getSearchDslNS.getSearchDsl).not.toHaveBeenCalled(); expect(client.search).not.toHaveBeenCalled(); }); + + it(`throws when ES is unable to find with missing Elasticsearch`, async () => { + await findNotSupportedServerError({ type }); + expect(client.search).toHaveBeenCalledTimes(1); + }); }); describe('returns', () => { @@ -3204,6 +3341,7 @@ describe('SavedObjectsRepository', () => { createGenericNotFoundEsUnavailableError(type, id) ); }; + it(`throws when options.namespace is '*'`, async () => { await expect( savedObjectsRepository.get(type, id, { namespace: ALL_NAMESPACES_STRING }) @@ -3222,11 +3360,7 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - { found: false }, - undefined, - { 'x-elastic-product': 'Elasticsearch' } - ) + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }, undefined) ); await expectNotFoundError(type, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -3234,11 +3368,7 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - {}, - { statusCode: 404 }, - { 'x-elastic-product': 'Elasticsearch' } - ) + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) ); await expectNotFoundError(type, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -3257,7 +3387,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES does not return the correct header when finding the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + undefined, + {} + ) ); await expectNotFoundEsUnavailableError(type, id); @@ -3367,8 +3501,7 @@ describe('SavedObjectsRepository', () => { client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise( { found: false }, - undefined, - { 'x-elastic-product': 'Elasticsearch' } + undefined ) // for actual target ); @@ -3421,10 +3554,16 @@ describe('SavedObjectsRepository', () => { describe('because alias is not used', () => { const expectExactMatchResult = async (aliasResult) => { const options = { namespace }; - client.update.mockResolvedValueOnce(aliasResult); // for alias object + if (!aliasResult.body) { + client.update.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { ...aliasResult }) + ); + } else { + client.update.mockResolvedValueOnce(aliasResult); // for alias object + } const response = getMockGetResponse({ type, id }, options.namespace); client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target + elasticsearchClientMock.createSuccessTransportRequestPromise({ ...response }) // for actual target ); const result = await savedObjectsRepository.resolve(type, id, options); @@ -3909,8 +4048,7 @@ describe('SavedObjectsRepository', () => { client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise( { ...mockGetResponse }, - { statusCode: 200 }, - { 'x-elastic-product': 'Elasticsearch' } + { statusCode: 200 } ) ); } @@ -3932,8 +4070,7 @@ describe('SavedObjectsRepository', () => { }, }, }, - { statusCode: 200 }, - { 'x-elastic-product': 'Elasticsearch' } + { statusCode: 200 } ) ); const result = await savedObjectsRepository.update(type, id, attributes, options); @@ -4144,11 +4281,7 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - { found: false }, - undefined, - { 'x-elastic-product': 'Elasticsearch' } - ) + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }, undefined) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -4156,11 +4289,7 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - {}, - { statusCode: 404 }, - { 'x-elastic-product': 'Elasticsearch' } - ) + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -4168,15 +4297,19 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get with missing Elasticsearch header`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + { statusCode: 404 }, + {} + ) ); await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); }); - it(`throws when ES is unable to find the index during get with missing Elasticsearch`, async () => { + it(`throws when ES is unable to find the index during get with missing Elasticsearch header`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }, {}) ); await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -4303,6 +4436,21 @@ describe('SavedObjectsRepository', () => { ); }; + const unsupportedProductExpectNotFoundError = async (type, options) => { + const results = generateResults(); + client.openPointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...results }, + { statusCode: 404 }, + {} + ) + ); + await expect( + savedObjectsRepository.openPointInTimeForType(type, options) + ).rejects.toThrowError(createGenericNotFoundEsUnavailableError()); + expect(client.openPointInTime).toHaveBeenCalledTimes(1); + }; + it(`throws when ES is unable to find the index`, async () => { client.openPointInTime.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) @@ -4321,6 +4469,11 @@ describe('SavedObjectsRepository', () => { await test(HIDDEN_TYPE); await test(['unknownType', HIDDEN_TYPE]); }); + + it(`throws on 404 with missing Elasticsearch product header`, async () => { + await unsupportedProductExpectNotFoundError(type); + expect(client.openPointInTime).toHaveBeenCalledTimes(1); + }); }); describe('returns', () => { diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 2e8e3189fae2d..365fc6a3734e4 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -14,7 +14,7 @@ import { REPOSITORY_RESOLVE_OUTCOME_STATS, } from '../../../core_usage_data'; import type { ElasticsearchClient } from '../../../elasticsearch/'; -import { isSupportedEsServer } from '../../../elasticsearch'; +import { isSupportedEsServer, isNotFoundFromUnsupportedServer } from '../../../elasticsearch'; import type { Logger } from '../../../logging'; import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { @@ -335,11 +335,15 @@ export class SavedObjectsRepository { require_alias: true, }; - const { body } = + const { body, statusCode, headers } = id && overwrite ? await this.client.index(requestParams) : await this.client.create(requestParams); + // throw if we can't verify a 404 response is from Elasticsearch + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(id, type); + } return this._rawToSavedObject({ ...raw, ...body, @@ -419,7 +423,16 @@ export class SavedObjectsRepository { { ignore: [404] } ) : undefined; - + // throw if we can't verify a 404 response is from Elasticsearch + if ( + bulkGetResponse && + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } let bulkRequestIndexCounter = 0; const bulkCreateParams: object[] = []; const expectedBulkResults: Either[] = expectedResults.map((expectedBulkGetResult) => { @@ -588,7 +601,16 @@ export class SavedObjectsRepository { { ignore: [404] } ) : undefined; - + // throw if we can't verify a 404 response is from Elasticsearch + if ( + bulkGetResponse && + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } const errors: SavedObjectsCheckConflictsResponse['errors'] = []; expectedBulkGetResults.forEach((expectedResult) => { if (isLeft(expectedResult)) { @@ -704,7 +726,7 @@ export class SavedObjectsRepository { const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); const typesToUpdate = allTypes.filter((type) => !this._registry.isNamespaceAgnostic(type)); - const { body } = await this.client.updateByQuery( + const { body, statusCode, headers } = await this.client.updateByQuery( { index: this.getIndicesForTypes(typesToUpdate), refresh: options.refresh, @@ -732,6 +754,10 @@ export class SavedObjectsRepository { }, { ignore: [404] } ); + // throw if we can't verify a 404 response is from Elasticsearch + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } return body; } @@ -876,10 +902,16 @@ export class SavedObjectsRepository { }, }; - const { body, statusCode } = await this.client.search(esOptions, { - ignore: [404], - }); + const { body, statusCode, headers } = await this.client.search( + esOptions, + { + ignore: [404], + } + ); if (statusCode === 404) { + if (!isSupportedEsServer(headers)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } // 404 is only possible here if the index is missing, which // we don't want to leak, see "404s from missing index" above return { @@ -975,7 +1007,16 @@ export class SavedObjectsRepository { { ignore: [404] } ) : undefined; - + // fail fast if we can't verify a 404 is from Elasticsearch + if ( + bulkGetResponse && + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } return { saved_objects: expectedBulkGetResults.map((expectedResult) => { if (isLeft(expectedResult)) { @@ -1099,7 +1140,18 @@ export class SavedObjectsRepository { }, { ignore: [404] } ); - + if ( + isNotFoundFromUnsupportedServer({ + statusCode: aliasResponse.statusCode, + headers: aliasResponse.headers, + }) + ) { + // throw if we cannot verify the response is from Elasticsearch + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError( + LEGACY_URL_ALIAS_TYPE, + rawAliasId + ); + } if ( aliasResponse.statusCode === 404 || aliasResponse.body.get?.found === false || @@ -1130,7 +1182,15 @@ export class SavedObjectsRepository { }, { ignore: [404] } ); - + // exit early if a 404 isn't from elasticsearch + if ( + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } const exactMatchDoc = bulkGetResponse?.body.docs[0]; const aliasMatchDoc = bulkGetResponse?.body.docs[1]; const foundExactMatch = @@ -1439,7 +1499,16 @@ export class SavedObjectsRepository { } ) : undefined; - + // fail fast if we can't verify a 404 response is from Elasticsearch + if ( + bulkGetResponse && + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } let bulkUpdateRequestIndexCounter = 0; const bulkUpdateParams: object[] = []; const expectedBulkUpdateResults: Either[] = expectedBulkGetResults.map( @@ -1580,7 +1649,7 @@ export class SavedObjectsRepository { // we need to target all SO indices as all types of objects may have references to the given SO. const targetIndices = this.getIndicesForTypes(allTypes); - const { body } = await this.client.updateByQuery( + const { body, statusCode, headers } = await this.client.updateByQuery( { index: targetIndices, refresh, @@ -1613,7 +1682,10 @@ export class SavedObjectsRepository { }, { ignore: [404] } ); - + // fail fast if we can't verify a 404 is from Elasticsearch + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } if (body.failures?.length) { throw SavedObjectsErrorHelpers.createConflictError( type, @@ -1878,11 +1950,15 @@ export class SavedObjectsRepository { ...(preference ? { preference } : {}), }; - const { body, statusCode } = await this.client.openPointInTime(esOptions, { + const { body, statusCode, headers } = await this.client.openPointInTime(esOptions, { ignore: [404], }); if (statusCode === 404) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(); + if (!isSupportedEsServer(headers)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } else { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(); + } } return { @@ -2061,7 +2137,7 @@ export class SavedObjectsRepository { throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); } - const { body, statusCode } = await this.client.get( + const { body, statusCode, headers } = await this.client.get( { id: this._serializer.generateRawId(undefined, type, id), index: this.getIndexForType(type), @@ -2077,6 +2153,9 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createConflictError(type, id); } return getSavedObjectNamespaces(namespace, body); + } else if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { + // checking if the 404 is from Elasticsearch + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); } return getSavedObjectNamespaces(namespace); } @@ -2108,9 +2187,7 @@ export class SavedObjectsRepository { const indexFound = statusCode !== 404; // check if we have the elasticsearch header when index is not found and if we do, ensure it is Elasticsearch - const esServerSupported = isSupportedEsServer(headers); - - if (!isFoundGetResponse(body) && !esServerSupported) { + if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); } @@ -2135,6 +2212,7 @@ export class SavedObjectsRepository { return { saved_object: object, outcome: 'exactMatch' }; } catch (err) { if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + // 404 responses already confirmed to be valid Elasticsearch responses await this.incrementResolveOutcomeStats(REPOSITORY_RESOLVE_OUTCOME_STATS.NOT_FOUND); } throw err; diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts index 11dbe6149878c..ba15fbabfba6b 100644 --- a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts @@ -23,6 +23,7 @@ import type { UpdateObjectsSpacesParams, } from './update_objects_spaces'; import { updateObjectsSpaces } from './update_objects_spaces'; +import { SavedObjectsErrorHelpers } from './errors'; type SetupParams = Partial< Pick @@ -105,6 +106,32 @@ describe('#updateObjectsSpaces', () => { }) ); } + /** Mocks the saved objects client so as to test unsupported server responding with 404 */ + function mockMgetResultsNotFound(...results: Array<{ found: boolean }>) { + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { + docs: results.map((x) => + x.found + ? { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + _source: { namespaces: [EXISTING_SPACE] }, + ...VERSION_PROPS, + found: true, + } + : { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + found: false, + } + ), + }, + { statusCode: 404 }, + {} + ) + ); + } /** Asserts that mget is called for the given objects */ function expectMgetArgs(...objects: SavedObjectsUpdateObjectsSpacesObject[]) { @@ -240,6 +267,17 @@ describe('#updateObjectsSpaces', () => { { ...obj7, spaces: [EXISTING_SPACE, 'foo-space'] }, ]); }); + + it('throws when mget not found response is missing the Elasticsearch header', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResultsNotFound({ found: true }); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrowError( + SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError() + ); + }); }); // Note: these test cases do not include requested objects that will result in errors (those are covered above) diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts index 3131d0240f96b..666b7b98b42e5 100644 --- a/src/core/server/saved_objects/service/lib/update_objects_spaces.ts +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts @@ -25,6 +25,7 @@ import { } from './internal_utils'; import { DEFAULT_REFRESH_SETTING } from './repository'; import type { RepositoryEsClient } from './repository_es_client'; +import { isNotFoundFromUnsupportedServer } from '../../../elasticsearch'; /** * An object that should have its spaces updated. @@ -190,6 +191,16 @@ export async function updateObjectsSpaces({ ) : undefined; + // fail fast if we can't verify a 404 response is from Elasticsearch + if ( + bulkGetResponse && + isNotFoundFromUnsupportedServer({ + statusCode: bulkGetResponse.statusCode, + headers: bulkGetResponse.headers, + }) + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(); + } const time = new Date().toISOString(); let bulkOperationRequestIndexCounter = 0; const bulkOperationParams: estypes.BulkOperationContainer[] = []; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ac5fe9a5d8dbb..cbe5f4e0cf1b6 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -7,45 +7,14 @@ import { AddConfigDeprecation } from '@kbn/config'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import Boom from '@hapi/boom'; -import { BulkIndexDocumentsParams } from 'elasticsearch'; import { ByteSizeValue } from '@kbn/config-schema'; -import { CatAliasesParams } from 'elasticsearch'; -import { CatAllocationParams } from 'elasticsearch'; -import { CatCommonParams } from 'elasticsearch'; -import { CatFielddataParams } from 'elasticsearch'; -import { CatHealthParams } from 'elasticsearch'; -import { CatHelpParams } from 'elasticsearch'; -import { CatIndicesParams } from 'elasticsearch'; -import { CatRecoveryParams } from 'elasticsearch'; -import { CatSegmentsParams } from 'elasticsearch'; -import { CatShardsParams } from 'elasticsearch'; -import { CatSnapshotsParams } from 'elasticsearch'; -import { CatTasksParams } from 'elasticsearch'; -import { CatThreadPoolParams } from 'elasticsearch'; -import { ClearScrollParams } from 'elasticsearch'; import { CliArgs } from '@kbn/config'; -import { Client } from 'elasticsearch'; import { ClientOptions } from '@elastic/elasticsearch'; -import { ClusterAllocationExplainParams } from 'elasticsearch'; -import { ClusterGetSettingsParams } from 'elasticsearch'; -import { ClusterHealthParams } from 'elasticsearch'; -import { ClusterPendingTasksParams } from 'elasticsearch'; -import { ClusterPutSettingsParams } from 'elasticsearch'; -import { ClusterRerouteParams } from 'elasticsearch'; -import { ClusterStateParams } from 'elasticsearch'; -import { ClusterStatsParams } from 'elasticsearch'; import { ConfigDeprecation } from '@kbn/config'; import { ConfigDeprecationFactory } from '@kbn/config'; import { ConfigDeprecationProvider } from '@kbn/config'; -import { ConfigOptions } from 'elasticsearch'; import { ConfigPath } from '@kbn/config'; import { ConfigService } from '@kbn/config'; -import { CountParams } from 'elasticsearch'; -import { CreateDocumentParams } from 'elasticsearch'; -import { DeleteDocumentByQueryParams } from 'elasticsearch'; -import { DeleteDocumentParams } from 'elasticsearch'; -import { DeleteScriptParams } from 'elasticsearch'; -import { DeleteTemplateParams } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; import { Duration as Duration_2 } from 'moment-timezone'; @@ -56,121 +25,34 @@ import { EcsEventOutcome } from '@kbn/logging'; import { EcsEventType } from '@kbn/logging'; import { EnvironmentMode } from '@kbn/config'; import { estypes } from '@elastic/elasticsearch'; -import { ExistsParams } from 'elasticsearch'; -import { ExplainParams } from 'elasticsearch'; -import { FieldStatsParams } from 'elasticsearch'; -import { GenericParams } from 'elasticsearch'; -import { GetParams } from 'elasticsearch'; -import { GetResponse as GetResponse_2 } from 'elasticsearch'; -import { GetScriptParams } from 'elasticsearch'; -import { GetSourceParams } from 'elasticsearch'; -import { GetTemplateParams } from 'elasticsearch'; import { IncomingHttpHeaders } from 'http'; -import { IndexDocumentParams } from 'elasticsearch'; -import { IndicesAnalyzeParams } from 'elasticsearch'; -import { IndicesClearCacheParams } from 'elasticsearch'; -import { IndicesCloseParams } from 'elasticsearch'; -import { IndicesCreateParams } from 'elasticsearch'; -import { IndicesDeleteAliasParams } from 'elasticsearch'; -import { IndicesDeleteParams } from 'elasticsearch'; -import { IndicesDeleteTemplateParams } from 'elasticsearch'; -import { IndicesExistsAliasParams } from 'elasticsearch'; -import { IndicesExistsParams } from 'elasticsearch'; -import { IndicesExistsTemplateParams } from 'elasticsearch'; -import { IndicesExistsTypeParams } from 'elasticsearch'; -import { IndicesFlushParams } from 'elasticsearch'; -import { IndicesFlushSyncedParams } from 'elasticsearch'; -import { IndicesForcemergeParams } from 'elasticsearch'; -import { IndicesGetAliasParams } from 'elasticsearch'; -import { IndicesGetFieldMappingParams } from 'elasticsearch'; -import { IndicesGetMappingParams } from 'elasticsearch'; -import { IndicesGetParams } from 'elasticsearch'; -import { IndicesGetSettingsParams } from 'elasticsearch'; -import { IndicesGetTemplateParams } from 'elasticsearch'; -import { IndicesGetUpgradeParams } from 'elasticsearch'; -import { IndicesOpenParams } from 'elasticsearch'; -import { IndicesPutAliasParams } from 'elasticsearch'; -import { IndicesPutMappingParams } from 'elasticsearch'; -import { IndicesPutSettingsParams } from 'elasticsearch'; -import { IndicesPutTemplateParams } from 'elasticsearch'; -import { IndicesRecoveryParams } from 'elasticsearch'; -import { IndicesRefreshParams } from 'elasticsearch'; -import { IndicesRolloverParams } from 'elasticsearch'; -import { IndicesSegmentsParams } from 'elasticsearch'; -import { IndicesShardStoresParams } from 'elasticsearch'; -import { IndicesShrinkParams } from 'elasticsearch'; -import { IndicesStatsParams } from 'elasticsearch'; -import { IndicesUpdateAliasesParams } from 'elasticsearch'; -import { IndicesUpgradeParams } from 'elasticsearch'; -import { IndicesValidateQueryParams } from 'elasticsearch'; -import { InfoParams } from 'elasticsearch'; -import { IngestDeletePipelineParams } from 'elasticsearch'; -import { IngestGetPipelineParams } from 'elasticsearch'; -import { IngestPutPipelineParams } from 'elasticsearch'; -import { IngestSimulateParams } from 'elasticsearch'; import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { Logger } from '@kbn/logging'; import { LoggerFactory } from '@kbn/logging'; import { LogLevel } from '@kbn/logging'; import { LogMeta } from '@kbn/logging'; import { LogRecord } from '@kbn/logging'; -import { MGetParams } from 'elasticsearch'; -import { MGetResponse } from 'elasticsearch'; -import { MSearchParams } from 'elasticsearch'; -import { MSearchResponse } from 'elasticsearch'; -import { MSearchTemplateParams } from 'elasticsearch'; -import { MTermVectorsParams } from 'elasticsearch'; -import { NodesHotThreadsParams } from 'elasticsearch'; -import { NodesInfoParams } from 'elasticsearch'; -import { NodesStatsParams } from 'elasticsearch'; import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; import { PathConfigType } from '@kbn/utils'; import { PeerCertificate } from 'tls'; -import { PingParams } from 'elasticsearch'; import { PublicMethodsOf } from '@kbn/utility-types'; -import { PutScriptParams } from 'elasticsearch'; -import { PutTemplateParams } from 'elasticsearch'; import { Readable } from 'stream'; import { RecursiveReadonly } from '@kbn/utility-types'; -import { ReindexParams } from 'elasticsearch'; -import { ReindexRethrottleParams } from 'elasticsearch'; -import { RenderSearchTemplateParams } from 'elasticsearch'; import { Request } from '@hapi/hapi'; import { RequestHandlerContext as RequestHandlerContext_2 } from 'src/core/server'; import { ResponseObject } from '@hapi/hapi'; import { ResponseToolkit } from '@hapi/hapi'; import { SchemaTypeError } from '@kbn/config-schema'; -import { ScrollParams } from 'elasticsearch'; -import { SearchParams } from 'elasticsearch'; -import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; -import { SearchShardsParams } from 'elasticsearch'; -import { SearchTemplateParams } from 'elasticsearch'; import { ShallowPromise } from '@kbn/utility-types'; -import { SnapshotCreateParams } from 'elasticsearch'; -import { SnapshotCreateRepositoryParams } from 'elasticsearch'; -import { SnapshotDeleteParams } from 'elasticsearch'; -import { SnapshotDeleteRepositoryParams } from 'elasticsearch'; -import { SnapshotGetParams } from 'elasticsearch'; -import { SnapshotGetRepositoryParams } from 'elasticsearch'; -import { SnapshotRestoreParams } from 'elasticsearch'; -import { SnapshotStatusParams } from 'elasticsearch'; -import { SnapshotVerifyRepositoryParams } from 'elasticsearch'; import { Stream } from 'stream'; -import { SuggestParams } from 'elasticsearch'; -import { TasksCancelParams } from 'elasticsearch'; -import { TasksGetParams } from 'elasticsearch'; -import { TasksListParams } from 'elasticsearch'; -import { TermvectorsParams } from 'elasticsearch'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UiCounterMetricType } from '@kbn/analytics'; -import { UpdateDocumentByQueryParams } from 'elasticsearch'; -import { UpdateDocumentParams } from 'elasticsearch'; import { URL } from 'url'; export { AddConfigDeprecation } @@ -196,24 +78,6 @@ export interface AppCategory { // @public (undocumented) export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RewriteAppenderConfig | RollingFileAppenderConfig; -// @public @deprecated (undocumented) -export interface AssistanceAPIResponse { - // (undocumented) - indices: { - [indexName: string]: { - action_required: MIGRATION_ASSISTANCE_INDEX_ACTION; - }; - }; -} - -// @public @deprecated (undocumented) -export interface AssistantAPIClientParams extends GenericParams { - // (undocumented) - method: 'GET'; - // (undocumented) - path: '/_migration/assistance'; -} - // @public @deprecated export interface AsyncPlugin { // (undocumented) @@ -862,38 +726,6 @@ export interface DeleteDocumentResponse { _version: number; } -// @public @deprecated (undocumented) -export interface DeprecationAPIClientParams extends GenericParams { - // (undocumented) - method: 'GET'; - // (undocumented) - path: '/_migration/deprecations'; -} - -// @public @deprecated (undocumented) -export interface DeprecationAPIResponse { - // (undocumented) - cluster_settings: DeprecationInfo[]; - // (undocumented) - index_settings: IndexSettingsDeprecationInfo; - // (undocumented) - ml_settings: DeprecationInfo[]; - // (undocumented) - node_settings: DeprecationInfo[]; -} - -// @public @deprecated (undocumented) -export interface DeprecationInfo { - // (undocumented) - details?: string; - // (undocumented) - level: MIGRATION_DEPRECATION_LEVEL; - // (undocumented) - message: string; - // (undocumented) - url: string; -} - // @public export interface DeprecationsClient { // Warning: (ae-forgotten-export) The symbol "DomainDeprecationDetails" needs to be exported by the entry point index.d.ts @@ -1019,8 +851,6 @@ export interface ElasticsearchServiceSetup { // @deprecated (undocumented) legacy: { readonly config$: Observable; - readonly createClient: (type: string, clientConfig?: Partial) => ILegacyCustomClusterClient; - readonly client: ILegacyClusterClient; }; } @@ -1031,8 +861,6 @@ export interface ElasticsearchServiceStart { // @deprecated (undocumented) legacy: { readonly config$: Observable; - readonly createClient: (type: string, clientConfig?: Partial) => ILegacyCustomClusterClient; - readonly client: ILegacyClusterClient; }; } @@ -1286,21 +1114,6 @@ export interface IKibanaSocket { }): Promise; } -// @public @deprecated -export type ILegacyClusterClient = Pick; - -// @public @deprecated -export type ILegacyCustomClusterClient = Pick; - -// @public @deprecated -export type ILegacyScopedClusterClient = Pick; - -// @public @deprecated (undocumented) -export interface IndexSettingsDeprecationInfo { - // (undocumented) - [indexName: string]: DeprecationInfo[]; -} - // @public (undocumented) export interface IRenderOptions { includeUserSettings?: boolean; @@ -1450,300 +1263,10 @@ export const kibanaResponseFactory: { // @public export type KnownHeaders = KnownKeys; -// @public @deprecated (undocumented) -export interface LegacyAPICaller { - // (undocumented) - (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'clearScroll', params: ClearScrollParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'count', params: CountParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'create', params: CreateDocumentParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'delete', params: DeleteDocumentParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'deleteScript', params: DeleteScriptParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'exists', params: ExistsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'explain', params: ExplainParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'fieldStats', params: FieldStatsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'get', params: GetParams, options?: LegacyCallAPIOptions): Promise>; - // (undocumented) - (endpoint: 'getScript', params: GetScriptParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'getSource', params: GetSourceParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'getTemplate', params: GetTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'index', params: IndexDocumentParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'info', params: InfoParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'mget', params: MGetParams, options?: LegacyCallAPIOptions): Promise>; - // (undocumented) - (endpoint: 'msearch', params: MSearchParams, options?: LegacyCallAPIOptions): Promise>; - // (undocumented) - (endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: LegacyCallAPIOptions): Promise>; - // (undocumented) - (endpoint: 'mtermvectors', params: MTermVectorsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'ping', params: PingParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'putScript', params: PutScriptParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'putTemplate', params: PutTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'reindex', params: ReindexParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'scroll', params: ScrollParams, options?: LegacyCallAPIOptions): Promise>; - // (undocumented) - (endpoint: 'search', params: SearchParams, options?: LegacyCallAPIOptions): Promise>; - // (undocumented) - (endpoint: 'searchShards', params: SearchShardsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'searchTemplate', params: SearchTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'suggest', params: SuggestParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'termvectors', params: TermvectorsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'update', params: UpdateDocumentParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.aliases', params: CatAliasesParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.allocation', params: CatAllocationParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.count', params: CatAllocationParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.fielddata', params: CatFielddataParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.health', params: CatHealthParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.help', params: CatHelpParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.indices', params: CatIndicesParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.master', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.nodes', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.plugins', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.recovery', params: CatRecoveryParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.repositories', params: CatCommonParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.segments', params: CatSegmentsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.shards', params: CatShardsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.tasks', params: CatTasksParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cluster.health', params: ClusterHealthParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cluster.state', params: ClusterStateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'cluster.stats', params: ClusterStatsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.close', params: IndicesCloseParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.create', params: IndicesCreateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.delete', params: IndicesDeleteParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.exists', params: IndicesExistsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.flush', params: IndicesFlushParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.get', params: IndicesGetParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.open', params: IndicesOpenParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.stats', params: IndicesStatsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'nodes.info', params: NodesInfoParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'nodes.stats', params: NodesStatsParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'snapshot.get', params: SnapshotGetParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'tasks.cancel', params: TasksCancelParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'tasks.get', params: TasksGetParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'tasks.list', params: TasksListParams, options?: LegacyCallAPIOptions): ReturnType; - // (undocumented) - (endpoint: 'transport.request', clientParams: AssistantAPIClientParams, options?: LegacyCallAPIOptions): Promise; - // (undocumented) - (endpoint: 'transport.request', clientParams: DeprecationAPIClientParams, options?: LegacyCallAPIOptions): Promise; - // (undocumented) - (endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; -} - -// @public @deprecated -export interface LegacyCallAPIOptions { - signal?: AbortSignal; - wrap401Errors?: boolean; -} - -// @public @deprecated -export class LegacyClusterClient implements ILegacyClusterClient { - constructor(config: LegacyElasticsearchClientConfig, log: Logger, type: string, getAuthHeaders?: GetAuthHeaders); - asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient; - // @deprecated - callAsInternalUser: LegacyAPICaller; - close(): void; - } - -// @public @deprecated (undocumented) -export type LegacyElasticsearchClientConfig = Pick & Pick & { - pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; - requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; - sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval']; - ssl?: Partial; -}; - -// @public -export interface LegacyElasticsearchError extends Boom.Boom { - // (undocumented) - [code_2]?: string; -} - -// @public -export class LegacyElasticsearchErrorHelpers { - // (undocumented) - static decorateNotAuthorizedError(error: Error, reason?: string): LegacyElasticsearchError; - // (undocumented) - static isNotAuthorizedError(error: any): error is LegacyElasticsearchError; -} - // @public @deprecated (undocumented) export interface LegacyRequest extends Request { } -// @public @deprecated -export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { - constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined); - // @deprecated - callAsCurrentUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; - // @deprecated - callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; - } - // Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts // // @public @@ -1793,12 +1316,6 @@ export interface MetricsServiceSetup { // @public export type MetricsServiceStart = MetricsServiceSetup; -// @public @deprecated (undocumented) -export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; - -// @public @deprecated (undocumented) -export type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; - // @public export type MutatingOperationRefreshSetting = boolean | 'wait_for'; @@ -2090,6 +1607,8 @@ export interface RegisterDeprecationsConfig { // @public export type RequestHandler

= (context: Context, request: KibanaRequest, response: ResponseFactory) => IKibanaResponse | Promise>; +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "LegacyScopedClusterClient" +// // @public export interface RequestHandlerContext { // (undocumented) @@ -2103,9 +1622,6 @@ export interface RequestHandlerContext { }; elasticsearch: { client: IScopedClusterClient; - legacy: { - client: ILegacyScopedClusterClient; - }; }; uiSettings: { client: IUiSettingsClient; @@ -2528,7 +2044,7 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) - static createGenericNotFoundEsUnavailableError(type: string, id: string): DecoratedError; + static createGenericNotFoundEsUnavailableError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) static createIndexAliasNotFoundError(alias: string): DecoratedError; // (undocumented) diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts index a6579069acbc0..b7c0733de728e 100644 --- a/src/core/server/status/plugins_status.test.ts +++ b/src/core/server/status/plugins_status.test.ts @@ -247,7 +247,6 @@ describe('PluginStatusService', () => { subscription.unsubscribe(); expect(statusUpdates).toEqual([ - { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } }, { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, @@ -274,7 +273,6 @@ describe('PluginStatusService', () => { subscription.unsubscribe(); expect(statusUpdates).toEqual([ - { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } }, { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, @@ -357,6 +355,35 @@ describe('PluginStatusService', () => { }).toThrowError(); }); + it('debounces plugins custom status registration', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + const available: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'a available', + }; + + const statusUpdates: Array> = []; + const subscription = service + .getDependenciesStatus$('b') + .subscribe((status) => statusUpdates.push(status)); + + const pluginA$ = new BehaviorSubject(available); + service.set('a', pluginA$); + + expect(statusUpdates).toStrictEqual([]); + + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + // Waiting for the debounce timeout should cut a new update + await delay(25); + subscription.unsubscribe(); + + expect(statusUpdates).toStrictEqual([{ a: available }]); + }); + it('debounces events in quick succession', async () => { const service = new PluginsStatusService({ core$: coreAllAvailable$, diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts index 6a8ef1081e165..7ef3ddb31d978 100644 --- a/src/core/server/status/plugins_status.ts +++ b/src/core/server/status/plugins_status.ts @@ -76,6 +76,7 @@ export class PluginsStatusService { public getDerivedStatus$(plugin: PluginName): Observable { return this.update$.pipe( + debounceTime(25), // Avoid calling the plugin's custom status logic for every plugin that depends on it. switchMap(() => { // Only go up the dependency tree if any of this plugin's dependencies have a custom status // Helps eliminate memory overhead of creating thousands of Observables unnecessarily. diff --git a/src/core/server/ui_settings/saved_objects/migrations.test.ts b/src/core/server/ui_settings/saved_objects/migrations.test.ts index cb10f9c7fd981..c454338f44c79 100644 --- a/src/core/server/ui_settings/saved_objects/migrations.test.ts +++ b/src/core/server/ui_settings/saved_objects/migrations.test.ts @@ -128,3 +128,38 @@ describe('ui_settings 7.13.0 migrations', () => { }); }); }); + +describe('ui_settings 8.0.0 migrations', () => { + const migration = migrations['8.0.0']; + + test('returns doc on empty object', () => { + expect(migration({} as SavedObjectUnsanitizedDoc)).toEqual({ + references: [], + }); + }); + test('removes ui_settings from deleted region_map and tile_map plugins', () => { + const doc = { + type: 'config', + id: '8.0.0', + attributes: { + buildNum: 9007199254740991, + 'visualization:regionmap:showWarnings': false, + 'visualization:tileMap:WMSdefaults': '{}', + 'visualization:tileMap:maxPrecision': 10, + }, + references: [], + updated_at: '2020-06-09T20:18:20.349Z', + migrationVersion: {}, + }; + expect(migration(doc)).toEqual({ + type: 'config', + id: '8.0.0', + attributes: { + buildNum: 9007199254740991, + }, + references: [], + updated_at: '2020-06-09T20:18:20.349Z', + migrationVersion: {}, + }); + }); +}); diff --git a/src/core/server/ui_settings/saved_objects/migrations.ts b/src/core/server/ui_settings/saved_objects/migrations.ts index b187c5f86dab0..e5d1a6bd1aa25 100644 --- a/src/core/server/ui_settings/saved_objects/migrations.ts +++ b/src/core/server/ui_settings/saved_objects/migrations.ts @@ -75,4 +75,27 @@ export const migrations = { }), references: doc.references || [], }), + '8.0.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({ + ...doc, + ...(doc.attributes && { + // owner: Team:Geo + attributes: Object.keys(doc.attributes).reduce( + (acc, key) => + [ + 'visualization:regionmap:showWarnings', + 'visualization:tileMap:WMSdefaults', + 'visualization:tileMap:maxPrecision', + ].includes(key) + ? { + ...acc, + } + : { + ...acc, + [key]: doc.attributes[key], + }, + {} + ), + }), + references: doc.references || [], + }), }; diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 0f073e383d2c8..1e9c0b443eb8b 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -20,7 +20,7 @@ export const CopySource: Task = { 'src/**', '!src/**/*.{test,test.mocks,mock}.{js,ts,tsx}', '!src/**/mocks.ts', // special file who imports .mock files - '!src/**/{target,__tests__,__snapshots__,__mocks__,integration_tests}/**', + '!src/**/{target,__tests__,__snapshots__,__mocks__,integration_tests,__fixtures__}/**', '!src/core/server/core_app/assets/favicons/favicon.distribution.png', '!src/core/server/core_app/assets/favicons/favicon.distribution.svg', '!src/test_utils/**', diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index e524d78a53e80..cf00241ee2766 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["management"], "optionalPlugins": ["home", "usageCollection"], - "requiredBundles": ["kibanaReact", "kibanaUtils", "home"], + "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "esUiShared"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 759e1f992808f..745452a31ff9c 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -8,7 +8,6 @@ import React, { PureComponent, Fragment } from 'react'; import classNames from 'classnames'; - import 'brace/theme/textmate'; import 'brace/mode/markdown'; import 'brace/mode/json'; @@ -19,7 +18,6 @@ import { EuiCodeBlock, EuiColorPicker, EuiScreenReaderOnly, - EuiCodeEditor, EuiDescribedFormGroup, EuiFieldNumber, EuiFieldText, @@ -40,6 +38,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { FieldSetting, FieldState } from '../../types'; import { isDefaultValue } from '../../lib'; import { UiSettingsType, DocLinksStart, ToastsStart } from '../../../../../../core/public'; +import { EuiCodeEditor } from '../../../../../es_ui_shared/public'; interface FieldProps { setting: FieldSetting; diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json index b207f600cbd4e..5bf4ce3d6248b 100644 --- a/src/plugins/advanced_settings/tsconfig.json +++ b/src/plugins/advanced_settings/tsconfig.json @@ -16,5 +16,6 @@ { "path": "../home/tsconfig.json" }, { "path": "../usage_collection/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, ] } diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json index ca43e4f258add..9452f43647a19 100644 --- a/src/plugins/console/kibana.json +++ b/src/plugins/console/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["devTools"], "optionalPlugins": ["usageCollection", "home"], "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"] diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 53a8e90a8c35c..3da299bb20095 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -183,27 +183,8 @@ export class DashboardPlugin const getStartServices = async () => { const [coreStart, deps] = await core.getStartServices(); - const useHideChrome = ({ toggleChrome } = { toggleChrome: true }) => { - React.useEffect(() => { - if (toggleChrome) { - coreStart.chrome.setIsVisible(false); - } - - return () => { - if (toggleChrome) { - coreStart.chrome.setIsVisible(true); - } - }; - }, [toggleChrome]); - }; - - const ExitFullScreenButton: React.FC< - ExitFullScreenButtonProps & { - toggleChrome: boolean; - } - > = ({ toggleChrome, ...props }) => { - useHideChrome({ toggleChrome }); - return ; + const ExitFullScreenButton: React.FC = (props) => { + return ; }; return { SavedObjectFinder: getSavedObjectFinder(coreStart.savedObjects, coreStart.uiSettings), diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index ee98a9ecf8892..6d84b3fd6eab4 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -29,8 +29,6 @@ import { isFilters as oldIsFilters, isExistsFilter as oldIsExistsFilter, isMatchAllFilter as oldIsMatchAllFilter, - isGeoBoundingBoxFilter as oldIsGeoBoundingBoxFilter, - isGeoPolygonFilter as oldIsGeoPolygonFilter, isMissingFilter as oldIsMissingFilter, isPhraseFilter as oldIsPhraseFilter, isPhrasesFilter as oldIsPhrasesFilter, @@ -49,14 +47,12 @@ import { RangeFilterMeta as oldRangeFilterMeta, RangeFilterParams as oldRangeFilterParams, ExistsFilter as oldExistsFilter, - GeoPolygonFilter as oldGeoPolygonFilter, PhrasesFilter as oldPhrasesFilter, PhraseFilter as oldPhraseFilter, MatchAllFilter as oldMatchAllFilter, CustomFilter as oldCustomFilter, MissingFilter as oldMissingFilter, RangeFilter as oldRangeFilter, - GeoBoundingBoxFilter as oldGeoBoundingBoxFilter, KueryNode as oldKueryNode, FilterMeta as oldFilterMeta, FILTERS as oldFILTERS, @@ -176,18 +172,6 @@ const isExistsFilter = oldIsExistsFilter; */ const isMatchAllFilter = oldIsMatchAllFilter; -/** - * @deprecated Import from the "@kbn/es-query" package directly instead. - * @removeBy 8.1 - */ -const isGeoBoundingBoxFilter = oldIsGeoBoundingBoxFilter; - -/** - * @deprecated Import from the "@kbn/es-query" package directly instead. - * @removeBy 8.1 - */ -const isGeoPolygonFilter = oldIsGeoPolygonFilter; - /** * @deprecated Import from the "@kbn/es-query" package directly instead. * @removeBy 8.1 @@ -338,12 +322,6 @@ type RangeFilterParams = oldRangeFilterParams; */ type ExistsFilter = oldExistsFilter; -/** - * @deprecated Import from the "@kbn/es-query" package directly instead. - * @removeBy 8.1 - */ -type GeoPolygonFilter = oldGeoPolygonFilter; - /** * @deprecated Import from the "@kbn/es-query" package directly instead. * @removeBy 8.1 @@ -380,12 +358,6 @@ type MissingFilter = oldMissingFilter; */ type RangeFilter = oldRangeFilter; -/** - * @deprecated Import from the "@kbn/es-query" package directly instead. - * @removeBy 8.1 - */ -type GeoBoundingBoxFilter = oldGeoBoundingBoxFilter; - /** * @deprecated Import from the "@kbn/es-query" package directly instead. * @removeBy 8.1 @@ -436,8 +408,6 @@ export { isFilters, isExistsFilter, isMatchAllFilter, - isGeoBoundingBoxFilter, - isGeoPolygonFilter, isMissingFilter, isPhraseFilter, isPhrasesFilter, @@ -463,14 +433,12 @@ export { RangeFilterMeta, RangeFilterParams, ExistsFilter, - GeoPolygonFilter, PhrasesFilter, PhraseFilter, MatchAllFilter, CustomFilter, MissingFilter, RangeFilter, - GeoBoundingBoxFilter, KueryNode, FilterMeta, IFieldSubType, diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index 340162e8bda70..f493b417b47ef 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -10,6 +10,6 @@ export * from './constants'; export * from './fields'; export * from './types'; export { IndexPatternsService, IndexPatternsContract } from './index_patterns'; -export type { IndexPattern } from './index_patterns'; +export type { IndexPattern, IndexPatternListItem } from './index_patterns'; export * from './errors'; export * from './expressions'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index a80e97b4e2cab..c6715fac5d9af 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -137,11 +137,11 @@ describe('IndexPatterns', () => { expect((await indexPatterns.get(id)).fields.length).toBe(1); }); - test('savedObjectCache pre-fetches only title', async () => { + test('savedObjectCache pre-fetches title, type, typeMeta', async () => { expect(await indexPatterns.getIds()).toEqual(['id']); expect(savedObjectsClient.find).toHaveBeenCalledWith({ type: 'index-pattern', - fields: ['title'], + fields: ['title', 'type', 'typeMeta'], perPage: 10000, }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 64628f7165f27..d20cfc98ba059 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -28,6 +28,7 @@ import { FieldAttrs, FieldSpec, IndexPatternFieldMap, + TypeMeta, } from '../types'; import { FieldFormatsStartCommon, FORMATS_UI_SETTINGS } from '../../../../field_formats/common/'; import { UI_SETTINGS, SavedObject } from '../../../common'; @@ -39,8 +40,21 @@ import { castEsToKbnFieldTypeName } from '../../kbn_field_types'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; -export interface IndexPatternSavedObjectAttrs { +export type IndexPatternSavedObjectAttrs = Pick< + IndexPatternAttributes, + 'title' | 'type' | 'typeMeta' +>; + +export type IndexPatternListSavedObjectAttrs = Pick< + IndexPatternAttributes, + 'title' | 'type' | 'typeMeta' +>; + +export interface IndexPatternListItem { + id: string; title: string; + type?: string; + typeMeta?: TypeMeta; } interface IndexPatternsServiceDeps { @@ -94,7 +108,7 @@ export class IndexPatternsService { private async refreshSavedObjectsCache() { const so = await this.savedObjectsClient.find({ type: INDEX_PATTERN_SAVED_OBJECT_TYPE, - fields: ['title'], + fields: ['title', 'type', 'typeMeta'], perPage: 10000, }); this.savedObjectsCache = so; @@ -152,9 +166,7 @@ export class IndexPatternsService { * Get list of index pattern ids with titles * @param refresh Force refresh of index pattern list */ - getIdsWithTitle = async ( - refresh: boolean = false - ): Promise> => { + getIdsWithTitle = async (refresh: boolean = false): Promise => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); } @@ -164,6 +176,8 @@ export class IndexPatternsService { return this.savedObjectsCache.map((obj) => ({ id: obj?.id, title: obj?.attributes?.title, + type: obj?.attributes?.type, + typeMeta: obj?.attributes?.typeMeta && JSON.parse(obj?.attributes?.typeMeta), })); }; @@ -559,7 +573,7 @@ export class IndexPatternsService { const createdIndexPattern = await this.initFromSavedObject(response); this.indexPatternCache.set(createdIndexPattern.id!, Promise.resolve(createdIndexPattern)); if (this.savedObjectsCache) { - this.savedObjectsCache.push(response as SavedObject); + this.savedObjectsCache.push(response as SavedObject); } return createdIndexPattern; } diff --git a/src/plugins/data/common/search/utils.ts b/src/plugins/data/common/search/utils.ts index e11957c6fa9fc..ea5ac28852d6a 100644 --- a/src/plugins/data/common/search/utils.ts +++ b/src/plugins/data/common/search/utils.ts @@ -12,7 +12,7 @@ import type { IKibanaSearchResponse } from './types'; * @returns true if response had an error while executing in ES */ export const isErrorResponse = (response?: IKibanaSearchResponse) => { - return !response || !response.rawResponse || (!response.isRunning && response.isPartial); + return !response || !response.rawResponse || (!response.isRunning && !!response.isPartial); }; /** diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 99c89ac69b795..0602f51889a6c 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -62,7 +62,7 @@ export const indexPatterns = { flattenHitWrapper, }; -export { IndexPatternsContract, IndexPattern, IndexPatternField } from './index_patterns'; +export { IndexPatternsContract, IndexPattern, IndexPatternField, TypeMeta } from './index_patterns'; export { IIndexPattern, @@ -79,6 +79,7 @@ export { INDEX_PATTERN_SAVED_OBJECT_TYPE, AggregationRestrictions, IndexPatternType, + IndexPatternListItem, } from '../common'; export { DuplicateIndexPatternError } from '../common/index_patterns/errors'; diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 1bdd17af2a78d..e23fc789656af 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -16,7 +16,7 @@ export { } from '../../common/index_patterns/lib'; export { flattenHitWrapper, formatHitProvider, onRedirectNoIndexPattern } from './index_patterns'; -export { IndexPatternField, IIndexPatternFieldList } from '../../common/index_patterns'; +export { IndexPatternField, IIndexPatternFieldList, TypeMeta } from '../../common/index_patterns'; export { IndexPatternsService, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 7da9f0293ec6f..9dd7dff9e5b66 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -748,7 +748,7 @@ export const esFilters: { isPhraseFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").PhraseFilter; isExistsFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").ExistsFilter; isPhrasesFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").PhrasesFilter; - isRangeFilter: (filter?: import("@kbn/es-query").ExistsFilter | import("@kbn/es-query").GeoPolygonFilter | import("@kbn/es-query").PhrasesFilter | import("@kbn/es-query").PhraseFilter | import("@kbn/es-query").MatchAllFilter | import("@kbn/es-query").MissingFilter | import("@kbn/es-query").RangeFilter | import("@kbn/es-query").GeoBoundingBoxFilter | undefined) => filter is import("@kbn/es-query").RangeFilter; + isRangeFilter: (filter?: import("@kbn/es-query").ExistsFilter | import("@kbn/es-query").PhrasesFilter | import("@kbn/es-query").PhraseFilter | import("@kbn/es-query").MatchAllFilter | import("@kbn/es-query").MissingFilter | import("@kbn/es-query").RangeFilter | undefined) => filter is import("@kbn/es-query").RangeFilter; isMatchAllFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").MatchAllFilter; isMissingFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query").MissingFilter; isQueryStringFilter: (filter: import("@kbn/es-query").FieldFilter) => filter is import("@kbn/es-query/target_types/filters/build_filters").QueryStringFilter; @@ -1269,7 +1269,6 @@ export class IndexPattern implements IIndexPattern { title: string; toSpec(): IndexPatternSpec; type: string | undefined; - // Warning: (ae-forgotten-export) The symbol "TypeMeta" needs to be exported by the entry point index.d.ts typeMeta?: TypeMeta; version: string | undefined; } @@ -1369,6 +1368,20 @@ export class IndexPatternField implements IFieldType { get visualizable(): boolean; } +// Warning: (ae-missing-release-tag) "IndexPatternListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface IndexPatternListItem { + // (undocumented) + id: string; + // (undocumented) + title: string; + // (undocumented) + type?: string; + // (undocumented) + typeMeta?: TypeMeta; +} + // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Input" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Arguments" needs to be exported by the entry point index.d.ts @@ -1457,19 +1470,14 @@ export class IndexPatternsService { fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; find: (search: string, size?: number) => Promise; get: (id: string) => Promise; - // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts - // // (undocumented) - getCache: () => Promise[] | null | undefined>; + getCache: () => Promise>[] | null | undefined>; getDefault: () => Promise; getDefaultId: () => Promise; getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; getFieldsForWildcard: (options: GetFieldsOptions) => Promise; getIds: (refresh?: boolean) => Promise; - getIdsWithTitle: (refresh?: boolean) => Promise>; + getIdsWithTitle: (refresh?: boolean) => Promise; getTitles: (refresh?: boolean) => Promise; refreshFields: (indexPattern: IndexPattern) => Promise; savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; @@ -1560,7 +1568,7 @@ export interface ISearchStartSearchSource { // Warning: (ae-missing-release-tag) "isErrorResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean | undefined; +export const isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean; // Warning: (ae-missing-release-tag) "isEsError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2057,8 +2065,8 @@ export const SEARCH_SESSIONS_MANAGEMENT_ID = "search_sessions"; // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "isClearable" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "isClearable" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "displayStyle">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts @@ -2275,6 +2283,18 @@ export type TimeRange = { mode?: 'absolute' | 'relative'; }; +// Warning: (ae-missing-release-tag) "TypeMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface TypeMeta { + // (undocumented) + aggs?: Record; + // (undocumented) + params?: { + rollup_index: string; + }; +} + // Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2334,20 +2354,20 @@ export interface WaitUntilNextSessionCompletesOptions { // src/plugins/data/public/index.ts:53:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:53:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:53:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:210:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:210:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:210:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:212:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:213:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:222:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:223:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:224:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:225:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:229:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:230:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:233:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:237:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:211:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:211:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:211:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:213:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:214:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:223:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:224:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:225:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:226:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:230:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:231:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:234:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:235:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:62:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/query/filter_manager/lib/map_filter.ts b/src/plugins/data/public/query/filter_manager/lib/map_filter.ts index 249c7bf47b8fb..d5e5d922d19d5 100644 --- a/src/plugins/data/public/query/filter_manager/lib/map_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/map_filter.ts @@ -17,8 +17,6 @@ import { mapRange } from './mappers/map_range'; import { mapExists } from './mappers/map_exists'; import { mapMissing } from './mappers/map_missing'; import { mapQueryString } from './mappers/map_query_string'; -import { mapGeoBoundingBox } from './mappers/map_geo_bounding_box'; -import { mapGeoPolygon } from './mappers/map_geo_polygon'; import { mapDefault } from './mappers/map_default'; import { generateMappingChain } from './generate_mapping_chain'; @@ -48,8 +46,6 @@ export function mapFilter(filter: Filter) { mapExists, mapMissing, mapQueryString, - mapGeoBoundingBox, - mapGeoPolygon, mapDefault, ]; diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.test.ts deleted file mode 100644 index aca6a345cb97e..0000000000000 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { mapGeoBoundingBox } from './map_geo_bounding_box'; -import { Filter, GeoBoundingBoxFilter } from '../../../../../common'; - -describe('filter manager utilities', () => { - describe('mapGeoBoundingBox()', () => { - test('should return the key and value for matching filters with bounds', async () => { - const filter = { - meta: { - index: 'logstash-*', - }, - geo_bounding_box: { - point: { - // field name - top_left: { lat: 5, lon: 10 }, - bottom_right: { lat: 15, lon: 20 }, - }, - }, - } as GeoBoundingBoxFilter; - - const result = mapGeoBoundingBox(filter); - - expect(result).toHaveProperty('key', 'point'); - expect(result).toHaveProperty('value'); - - if (result.value) { - const displayName = result.value(); - // remove html entities and non-alphanumerics to get the gist of the value - expect(displayName.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe( - 'lat5lon10tolat15lon20' - ); - } - }); - - test('should return the key and value even when using ignore_unmapped', async () => { - const filter = { - meta: { - index: 'logstash-*', - }, - geo_bounding_box: { - ignore_unmapped: true, - point: { - // field name - top_left: { lat: 5, lon: 10 }, - bottom_right: { lat: 15, lon: 20 }, - }, - }, - } as GeoBoundingBoxFilter; - - const result = mapGeoBoundingBox(filter); - - expect(result).toHaveProperty('key', 'point'); - expect(result).toHaveProperty('value'); - - if (result.value) { - const displayName = result.value(); - // remove html entities and non-alphanumerics to get the gist of the value - expect(displayName.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe( - 'lat5lon10tolat15lon20' - ); - } - }); - - test('should return undefined for none matching', async (done) => { - const filter = { - meta: { index: 'logstash-*' }, - query: { query_string: { query: 'foo:bar' } }, - } as Filter; - - try { - mapGeoBoundingBox(filter); - } catch (e) { - expect(e).toBe(filter); - done(); - } - }); - }); -}); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.ts deleted file mode 100644 index dfa8e862e1b4c..0000000000000 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_bounding_box.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { GeoBoundingBoxFilter, FILTERS, isGeoBoundingBoxFilter, Filter } from '@kbn/es-query'; - -import { FilterValueFormatter } from '../../../../../common'; - -const getFormattedValueFn = (params: any) => { - return (formatter?: FilterValueFormatter) => { - const corners = formatter - ? { - topLeft: formatter.convert(params.top_left), - bottomRight: formatter.convert(params.bottom_right), - } - : { - topLeft: JSON.stringify(params.top_left), - bottomRight: JSON.stringify(params.bottom_right), - }; - - return corners.topLeft + ' to ' + corners.bottomRight; - }; -}; - -const getParams = (filter: GeoBoundingBoxFilter) => { - const key = Object.keys(filter.geo_bounding_box).filter((k) => k !== 'ignore_unmapped')[0]; - const params = filter.geo_bounding_box[key]; - - return { - key, - params, - type: FILTERS.GEO_BOUNDING_BOX, - value: getFormattedValueFn(params), - }; -}; - -export const mapGeoBoundingBox = (filter: Filter) => { - if (!isGeoBoundingBoxFilter(filter)) { - throw filter; - } - - return getParams(filter); -}; diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.test.ts deleted file mode 100644 index c8fba3adcbb85..0000000000000 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { mapGeoPolygon } from './map_geo_polygon'; -import { Filter, GeoPolygonFilter } from '../../../../../common'; - -describe('filter manager utilities', () => { - let filter: GeoPolygonFilter; - - beforeEach(() => { - filter = { - meta: { - index: 'logstash-*', - }, - geo_polygon: { - point: { - points: [ - { lat: 5, lon: 10 }, - { lat: 15, lon: 20 }, - ], - }, - }, - } as GeoPolygonFilter; - }); - - describe('mapGeoPolygon()', () => { - test('should return the key and value for matching filters with bounds', async () => { - const result = mapGeoPolygon(filter); - - expect(result).toHaveProperty('key', 'point'); - expect(result).toHaveProperty('value'); - - if (result.value) { - const displayName = result.value(); - // remove html entities and non-alphanumerics to get the gist of the value - expect(displayName.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe( - 'lat5lon10lat15lon20' - ); - } - }); - - test('should return the key and value even when using ignore_unmapped', async () => { - const result = mapGeoPolygon(filter); - - expect(result).toHaveProperty('key', 'point'); - expect(result).toHaveProperty('value'); - - if (result.value) { - const displayName = result.value(); - // remove html entities and non-alphanumerics to get the gist of the value - expect(displayName.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe( - 'lat5lon10lat15lon20' - ); - } - }); - - test('should return undefined for none matching', async (done) => { - const wrongFilter = { - meta: { index: 'logstash-*' }, - query: { query_string: { query: 'foo:bar' } }, - } as Filter; - - try { - mapGeoPolygon(wrongFilter); - } catch (e) { - expect(e).toBe(wrongFilter); - - done(); - } - }); - }); -}); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.ts deleted file mode 100644 index e1c21aae64da6..0000000000000 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { GeoPolygonFilter, FILTERS, Filter, isGeoPolygonFilter } from '@kbn/es-query'; - -import { FilterValueFormatter } from '../../../../../common'; - -const POINTS_SEPARATOR = ', '; - -const getFormattedValueFn = (points: string[]) => { - return (formatter?: FilterValueFormatter) => { - return points - .map((point: string) => (formatter ? formatter.convert(point) : JSON.stringify(point))) - .join(POINTS_SEPARATOR); - }; -}; - -function getParams(filter: GeoPolygonFilter) { - const key = Object.keys(filter.geo_polygon).filter((k) => k !== 'ignore_unmapped')[0]; - const params = filter.geo_polygon[key]; - - return { - key, - params, - type: FILTERS.GEO_POLYGON, - value: getFormattedValueFn(params.points || []), - }; -} - -export function mapGeoPolygon(filter: Filter) { - if (!isGeoPolygonFilter(filter)) { - throw filter; - } - return getParams(filter); -} diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss index d45f7040e5739..24f3ca05a5685 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss @@ -11,6 +11,10 @@ padding-bottom: $euiSizeS; } +.globalQueryBar--inPage { + padding: 0; +} + .globalFilterGroup__filterBar { margin-top: $euiSizeXS; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index 5055e122e199a..6debf34c2ae8c 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -57,20 +57,6 @@ export default function FilterLabel({ filter, valueLabel, filterLabelStatus }: F {filter.meta.key}: {getValue(`${existsOperator.message}`)} ); - case FILTERS.GEO_BOUNDING_BOX: - return ( - - {prefix} - {filter.meta.key}: {getValue(valueLabel)} - - ); - case FILTERS.GEO_POLYGON: - return ( - - {prefix} - {filter.meta.key}: {getValue(valueLabel)} - - ); case FILTERS.PHRASES: return ( diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx index 077b9ac47286d..d0221658f3e08 100644 --- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx +++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx @@ -143,7 +143,7 @@ export function SaveQueryForm({ })} helpText={i18n.translate('data.search.searchBar.savedQueryNameHelpText', { defaultMessage: - 'Name is required. Name cannot contain leading or trailing whitespace. Name must be unique.', + 'Name is required, it cannot contain leading or trailing whitespace and must be unique.', })} isInvalid={hasErrors} > diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index a03e7b33d2b65..db0bebf97578b 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -75,6 +75,8 @@ export interface SearchBarOwnProps { iconType?: EuiIconProps['type']; nonKqlMode?: 'lucene' | 'text'; nonKqlModeHelpText?: string; + // defines padding; use 'inPage' to avoid extra padding; use 'detached' if the searchBar appears at the very top of the view, without any wrapper + displayStyle?: 'inPage' | 'detached'; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -410,8 +412,12 @@ class SearchBarUI extends Component { ); } + const globalQueryBarClasses = classNames('globalQueryBar', { + 'globalQueryBar--inPage': this.props.displayStyle === 'inPage', + }); + return ( -

+
{queryBar} {filterBar} diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 71863ecb61341..7eafad71f4f95 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -517,20 +517,16 @@ class IndexPatternsService { fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; find: (search: string, size?: number) => Promise; get: (id: string) => Promise; - // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts - // // (undocumented) - getCache: () => Promise[] | null | undefined>; + getCache: () => Promise>[] | null | undefined>; getDefault: () => Promise; getDefaultId: () => Promise; getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts getFieldsForWildcard: (options: GetFieldsOptions) => Promise; getIds: (refresh?: boolean) => Promise; - getIdsWithTitle: (refresh?: boolean) => Promise>; + // Warning: (ae-forgotten-export) The symbol "IndexPatternListItem" needs to be exported by the entry point index.d.ts + getIdsWithTitle: (refresh?: boolean) => Promise; getTitles: (refresh?: boolean) => Promise; refreshFields: (indexPattern: IndexPattern) => Promise; savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; @@ -697,7 +693,7 @@ export class Plugin implements Plugin_2, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; diff --git a/src/plugins/dev_tools/kibana.json b/src/plugins/dev_tools/kibana.json index f1c6c9ecf87e6..75a1e82f1d910 100644 --- a/src/plugins/dev_tools/kibana.json +++ b/src/plugins/dev_tools/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["urlForwarding"] } diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts index 130b43539d9b5..9b69a98ca7996 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts @@ -78,9 +78,7 @@ export function getStateColumnActions({ state: DiscoverState | ContextState; }) { function onAddColumn(columnName: string) { - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } + popularizeField(indexPattern, columnName, indexPatterns, capabilities); const columns = addColumn(state.columns || [], columnName, useNewFieldsApi); const defaultOrder = config.get(SORT_DEFAULT_ORDER_SETTING); const sort = @@ -89,9 +87,7 @@ export function getStateColumnActions({ } function onRemoveColumn(columnName: string) { - if (capabilities.discover.save) { - popularizeField(indexPattern, columnName, indexPatterns); - } + popularizeField(indexPattern, columnName, indexPatterns, capabilities); const columns = removeColumn(state.columns || [], columnName, useNewFieldsApi); // The state's sort property is an array of [sortByColumn,sortDirection] const sort = diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 94e28c3f1d54c..6d241468bdf74 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -122,7 +122,7 @@ export function DiscoverLayout({ const onAddFilter = useCallback( (field: IndexPatternField | string, values: string, operation: '+' | '-') => { const fieldName = typeof field === 'string' ? field : field.name; - popularizeField(indexPattern, fieldName, indexPatterns); + popularizeField(indexPattern, fieldName, indexPatterns, capabilities); const newFilters = esFilters.generateFilters( filterManager, field, @@ -135,7 +135,7 @@ export function DiscoverLayout({ } return filterManager.addFilters(newFilters); }, - [filterManager, indexPattern, indexPatterns, trackUiMetric] + [filterManager, indexPattern, indexPatterns, trackUiMetric, capabilities] ); const onEditRuntimeField = useCallback(() => { diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap index 2c2674b158bfc..6043a5d382598 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`render 1`] = ` +exports[`OpenSearchPanel render 1`] = ` { return { getServices: () => ({ core: { uiSettings: {}, savedObjects: {} }, addBasePath: (path: string) => path, + capabilities: mockCapabilities(), }), }; }); import { OpenSearchPanel } from './open_search_panel'; -test('render', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); +describe('OpenSearchPanel', () => { + test('render', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('should not render manage searches button without permissions', () => { + mockCapabilities.mockReturnValue({ + savedObjectsManagement: { + edit: false, + delete: false, + }, + }); + const component = shallow(); + expect(component.find('[data-test-subj="manageSearches"]').exists()).toBe(false); + }); }); diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx index e63f010a8dffc..31026a1e0ab59 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx @@ -34,8 +34,12 @@ export function OpenSearchPanel(props: OpenSearchPanelProps) { const { core: { uiSettings, savedObjects }, addBasePath, + capabilities, } = getServices(); + const hasSavedObjectPermission = + capabilities.savedObjectsManagement?.edit || capabilities.savedObjectsManagement?.delete; + return ( @@ -73,25 +77,28 @@ export function OpenSearchPanel(props: OpenSearchPanelProps) { savedObjects={savedObjects} /> - - - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - - - - - - + {hasSavedObjectPermission && ( + + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + + + + + + )} ); } diff --git a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx index 7ac6a9d0e8de3..a21b035c335df 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx @@ -54,6 +54,9 @@ describe('ContextApp test', () => { discover: { save: true, }, + indexPatterns: { + save: true, + }, }, indexPatterns: indexPatternsMock, toastNotifications: { addDanger: () => {} }, diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 37963eb2dfa93..25590f331839e 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -109,10 +109,10 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp filterManager.addFilters(newFilters); if (indexPatterns) { const fieldName = typeof field === 'string' ? field : field.name; - await popularizeField(indexPattern, fieldName, indexPatterns); + await popularizeField(indexPattern, fieldName, indexPatterns, capabilities); } }, - [filterManager, indexPatternId, indexPatterns, indexPattern] + [filterManager, indexPatternId, indexPatterns, indexPattern, capabilities] ); const TopNavMenu = navigation.ui.TopNavMenu; diff --git a/src/plugins/discover/public/application/helpers/popularize_field.test.ts b/src/plugins/discover/public/application/helpers/popularize_field.test.ts index 8be23c4270438..7ae3994abd21a 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.test.ts +++ b/src/plugins/discover/public/application/helpers/popularize_field.test.ts @@ -6,15 +6,27 @@ * Side Public License, v 1. */ +import { Capabilities } from 'kibana/public'; import { IndexPattern, IndexPatternsService } from '../../../../data/public'; import { popularizeField } from './popularize_field'; +const capabilities = ({ + indexPatterns: { + save: true, + }, +} as unknown) as Capabilities; + describe('Popularize field', () => { test('returns undefined if index pattern lacks id', async () => { const indexPattern = ({} as unknown) as IndexPattern; const fieldName = '@timestamp'; const indexPatternsService = ({} as unknown) as IndexPatternsService; - const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + const result = await popularizeField( + indexPattern, + fieldName, + indexPatternsService, + capabilities + ); expect(result).toBeUndefined(); }); @@ -26,7 +38,12 @@ describe('Popularize field', () => { } as unknown) as IndexPattern; const fieldName = '@timestamp'; const indexPatternsService = ({} as unknown) as IndexPatternsService; - const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + const result = await popularizeField( + indexPattern, + fieldName, + indexPatternsService, + capabilities + ); expect(result).toBeUndefined(); }); @@ -44,7 +61,12 @@ describe('Popularize field', () => { const indexPatternsService = ({ updateSavedObject: async () => {}, } as unknown) as IndexPatternsService; - const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + const result = await popularizeField( + indexPattern, + fieldName, + indexPatternsService, + capabilities + ); expect(result).toBeUndefined(); expect(field.count).toEqual(1); }); @@ -65,7 +87,34 @@ describe('Popularize field', () => { throw new Error('unknown error'); }, } as unknown) as IndexPatternsService; - const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + const result = await popularizeField( + indexPattern, + fieldName, + indexPatternsService, + capabilities + ); + expect(result).toBeUndefined(); + }); + + test('should not try to update index pattern without permissions', async () => { + const field = { + count: 0, + }; + const indexPattern = ({ + id: 'id', + fields: { + getByName: () => field, + }, + } as unknown) as IndexPattern; + const fieldName = '@timestamp'; + const indexPatternsService = ({ + updateSavedObject: jest.fn(), + } as unknown) as IndexPatternsService; + const result = await popularizeField(indexPattern, fieldName, indexPatternsService, ({ + indexPatterns: { save: false }, + } as unknown) as Capabilities); expect(result).toBeUndefined(); + expect(indexPatternsService.updateSavedObject).not.toHaveBeenCalled(); + expect(field.count).toEqual(0); }); }); diff --git a/src/plugins/discover/public/application/helpers/popularize_field.ts b/src/plugins/discover/public/application/helpers/popularize_field.ts index 4ade7d1768419..90968dd7c3d58 100644 --- a/src/plugins/discover/public/application/helpers/popularize_field.ts +++ b/src/plugins/discover/public/application/helpers/popularize_field.ts @@ -6,14 +6,16 @@ * Side Public License, v 1. */ +import type { Capabilities } from 'kibana/public'; import { IndexPattern, IndexPatternsContract } from '../../../../data/public'; async function popularizeField( indexPattern: IndexPattern, fieldName: string, - indexPatternsService: IndexPatternsContract + indexPatternsService: IndexPatternsContract, + capabilities: Capabilities ) { - if (!indexPattern.id) return; + if (!indexPattern.id || !capabilities?.indexPatterns?.save) return; const field = indexPattern.fields.getByName(fieldName); if (!field) { return; diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 1ecf76dbbd5c2..42dc716fe64e9 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -3,16 +3,11 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "inspector", - "uiActions" - ], - "extraPublicDirs": [ - "public/lib/test_samples" - ], - "requiredBundles": [ - "savedObjects", - "kibanaReact", - "kibanaUtils" - ] + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "requiredPlugins": ["inspector", "uiActions"], + "extraPublicDirs": ["public/lib/test_samples"], + "requiredBundles": ["savedObjects", "kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/embeddable/public/lib/state_transfer/types.ts b/src/plugins/embeddable/public/lib/state_transfer/types.ts index 98cf6e70284cd..74ee31ba71104 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/types.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/types.ts @@ -17,6 +17,7 @@ export const EMBEDDABLE_EDITOR_STATE_KEY = 'embeddable_editor_state'; */ export interface EmbeddableEditorState { originatingApp: string; + originatingPath?: string; embeddableId?: string; valueInput?: EmbeddableInput; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 2e46cb82dc592..3dfe10445fb85 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -369,6 +369,8 @@ export interface EmbeddableEditorState { embeddableId?: string; // (undocumented) originatingApp: string; + // (undocumented) + originatingPath?: string; searchSessionId?: string; // (undocumented) valueInput?: EmbeddableInput; diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json index d442bfb93d5af..2735b153f738c 100644 --- a/src/plugins/es_ui_shared/kibana.json +++ b/src/plugins/es_ui_shared/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "ui": true, "server": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "extraPublicDirs": [ "static/validators/string", "static/forms/hook_form_lib", @@ -10,7 +14,5 @@ "static/forms/components", "static/forms/helpers/field_validators/types" ], - "requiredBundles": [ - "data" - ] + "requiredBundles": ["data"] } diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json index 23c7fe722fdb3..46e6ef8b4ea75 100644 --- a/src/plugins/expressions/kibana.json +++ b/src/plugins/expressions/kibana.json @@ -3,9 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "extraPublicDirs": ["common", "common/fonts"], - "requiredBundles": [ - "kibanaUtils", - "inspector" - ] + "requiredBundles": ["kibanaUtils", "inspector"] } diff --git a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap index 4e66fd9e14c81..348f618805858 100644 --- a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap @@ -4,6 +4,7 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = `
{ defaultProps.localStorage.getItem = sinon.spy(() => 'true'); const component = await renderHome({ - find: () => Promise.resolve({ total: 0 }), + http: { + get: () => Promise.resolve({ isNewInstance: true }), + }, }); sinon.assert.calledOnce(defaultProps.localStorage.getItem); diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index cb02d62f9164f..da8eac6c78a8d 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -33,6 +33,7 @@ export function HomeApp({ directories, solutions }) { addBasePath, environmentService, telemetry, + http, } = getServices(); const environment = environmentService.getEnvironment(); const isCloudEnabled = environment.cloud; @@ -71,10 +72,10 @@ export function HomeApp({ directories, solutions }) { addBasePath={addBasePath} directories={directories} solutions={solutions} - find={savedObjectsClient.find} localStorage={localStorage} urlBasePath={getBasePath()} telemetry={telemetry} + http={http} /> diff --git a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solutions_section.test.tsx.snap b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solutions_section.test.tsx.snap index cef62f4a1e88d..676745e7f1a52 100644 --- a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solutions_section.test.tsx.snap +++ b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solutions_section.test.tsx.snap @@ -19,7 +19,6 @@ exports[`SolutionsSection renders a single solution 1`] = ` = ({ addBasePath, solutions }) => { - + {solutions.map((solution) => ( ))} diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index 55b733e413f6a..ca7e6874c75c2 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -119,7 +119,7 @@ export class Welcome extends React.Component { const { urlBasePath, telemetry } = this.props; return ( -
+
diff --git a/src/plugins/home/server/routes/fetch_new_instance_status.ts b/src/plugins/home/server/routes/fetch_new_instance_status.ts new file mode 100644 index 0000000000000..12d94feb3b8a1 --- /dev/null +++ b/src/plugins/home/server/routes/fetch_new_instance_status.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IRouter } from 'src/core/server'; +import { isNewInstance } from '../services/new_instance_status'; + +export const registerNewInstanceStatusRoute = (router: IRouter) => { + router.get( + { + path: '/internal/home/new_instance_status', + validate: false, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { client: soClient } = context.core.savedObjects; + const { client: esClient } = context.core.elasticsearch; + + try { + return res.ok({ + body: { + isNewInstance: await isNewInstance({ esClient, soClient }), + }, + }); + } catch (e) { + return res.customError({ + statusCode: 500, + }); + } + }) + ); +}; diff --git a/src/plugins/home/server/routes/index.ts b/src/plugins/home/server/routes/index.ts index 905304e059660..6013dbf130831 100644 --- a/src/plugins/home/server/routes/index.ts +++ b/src/plugins/home/server/routes/index.ts @@ -8,7 +8,9 @@ import { IRouter } from 'src/core/server'; import { registerHitsStatusRoute } from './fetch_es_hits_status'; +import { registerNewInstanceStatusRoute } from './fetch_new_instance_status'; export const registerRoutes = (router: IRouter) => { registerHitsStatusRoute(router); + registerNewInstanceStatusRoute(router); }; diff --git a/src/plugins/home/server/services/new_instance_status.test.ts b/src/plugins/home/server/services/new_instance_status.test.ts new file mode 100644 index 0000000000000..9ce8f8571f5a1 --- /dev/null +++ b/src/plugins/home/server/services/new_instance_status.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isNewInstance } from './new_instance_status'; +import { elasticsearchServiceMock, savedObjectsClientMock } from '../../../../core/server/mocks'; + +describe('isNewInstance', () => { + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + const soClient = savedObjectsClientMock.create(); + + beforeEach(() => jest.resetAllMocks()); + + it('returns true when there are no index patterns', async () => { + soClient.find.mockResolvedValue({ + page: 1, + per_page: 100, + total: 0, + saved_objects: [], + }); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns false when there are any index patterns other than metrics-* or logs-*', async () => { + soClient.find.mockResolvedValue({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: '1', + references: [], + type: 'index-pattern', + score: 99, + attributes: { title: 'my-pattern-*' }, + }, + ], + }); + expect(await isNewInstance({ esClient, soClient })).toEqual(false); + }); + + describe('when only metrics-* and logs-* index patterns exist', () => { + beforeEach(() => { + soClient.find.mockResolvedValue({ + page: 1, + per_page: 100, + total: 2, + saved_objects: [ + { + id: '1', + references: [], + type: 'index-pattern', + score: 99, + attributes: { title: 'metrics-*' }, + }, + { + id: '2', + references: [], + type: 'index-pattern', + score: 99, + attributes: { title: 'logs-*' }, + }, + ], + }); + }); + + it('calls /_cat/indices for the index patterns', async () => { + await isNewInstance({ esClient, soClient }); + expect(esClient.asCurrentUser.cat.indices).toHaveBeenCalledWith({ + index: 'logs-*,metrics-*', + format: 'json', + }); + }); + + it('returns true if no logs or metrics indices exist', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns true if no logs or metrics indices contain data', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([ + { index: '.ds-metrics-foo', 'docs.count': '0' }, + ]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns true if only metrics-elastic_agent index contains data', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([ + { index: '.ds-metrics-elastic_agent', 'docs.count': '100' }, + ]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns true if only logs-elastic_agent index contains data', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([ + { index: '.ds-logs-elastic_agent', 'docs.count': '100' }, + ]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns false if any other logs or metrics indices contain data', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([ + { index: '.ds-metrics-foo', 'docs.count': '100' }, + ]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(false); + }); + + it('returns false if an authentication error is thrown', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createErrorTransportRequestPromise({}) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(false); + }); + }); +}); diff --git a/src/plugins/home/server/services/new_instance_status.ts b/src/plugins/home/server/services/new_instance_status.ts new file mode 100644 index 0000000000000..00223589a8d41 --- /dev/null +++ b/src/plugins/home/server/services/new_instance_status.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { IScopedClusterClient, SavedObjectsClientContract } from '../../../../core/server'; +import type { IndexPatternSavedObjectAttrs } from '../../../data/common/index_patterns/index_patterns'; + +const LOGS_INDEX_PATTERN = 'logs-*'; +const METRICS_INDEX_PATTERN = 'metrics-*'; + +const INDEX_PREFIXES_TO_IGNORE = [ + '.ds-metrics-elastic_agent', // ignore index created by Fleet server itself + '.ds-logs-elastic_agent', // ignore index created by Fleet server itself +]; + +interface Deps { + esClient: IScopedClusterClient; + soClient: SavedObjectsClientContract; +} + +export const isNewInstance = async ({ esClient, soClient }: Deps): Promise => { + const indexPatterns = await soClient.find({ + type: 'index-pattern', + fields: ['title'], + search: `*`, + searchFields: ['title'], + perPage: 100, + }); + + // If there are no index patterns, assume this is a new instance + if (indexPatterns.total === 0) { + return true; + } + + // If there are any index patterns that are not the default metrics-* and logs-* ones created by Fleet, assume this + // is not a new instance + if ( + indexPatterns.saved_objects.some( + (ip) => + ip.attributes.title !== LOGS_INDEX_PATTERN && ip.attributes.title !== METRICS_INDEX_PATTERN + ) + ) { + return false; + } + + try { + const logsAndMetricsIndices = await esClient.asCurrentUser.cat.indices({ + index: `${LOGS_INDEX_PATTERN},${METRICS_INDEX_PATTERN}`, + format: 'json', + }); + + const anyIndicesContainerUserData = logsAndMetricsIndices.body + // Ignore some data that is shipped by default + .filter(({ index }) => !INDEX_PREFIXES_TO_IGNORE.some((prefix) => index?.startsWith(prefix))) + // If any other logs and metrics indices have data, return false + .some((catResult) => (catResult['docs.count'] ?? '0') !== '0'); + + return !anyIndicesContainerUserData; + } catch (e) { + // If any errors are encountered return false to be safe + return false; + } +}; diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index 6eb5c9fe38bad..8227e48501aa2 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -212,7 +212,7 @@ export const getSavedObjects = (): SavedObject[] => [ fieldFormatMap: '{"hour_of_day":{"id":"number","params":{"pattern":"00"}},"AvgTicketPrice":{"id":"number","params":{"pattern":"$0,0.[00]"}}}', runtimeFieldMap: - '{"hour_of_day":{"type":"long","script":{"source":"emit(doc[\'timestamp\'].value.hourOfDay);"}}}', + '{"hour_of_day":{"type":"long","script":{"source":"emit(doc[\'timestamp\'].value.getHour());"}}}', }, references: [], }, diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx index 3b06fa1cff298..80224dbfb673f 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx @@ -36,7 +36,7 @@ export const EmptyPrompts: FC = ({ loadSources, }) => { const { - services: { docLinks, application, http }, + services: { docLinks, application, http, searchClient }, } = useKibana(); const [remoteClustersExist, setRemoteClustersExist] = useState(false); @@ -47,7 +47,13 @@ export const EmptyPrompts: FC = ({ useCallback(() => { let isMounted = true; if (!hasDataIndices) - getIndices(http, () => false, '*:*', false).then((dataSources) => { + getIndices({ + http, + isRollupIndex: () => false, + pattern: '*:*', + showAllIndices: false, + searchClient, + }).then((dataSources) => { if (isMounted) { setRemoteClustersExist(!!dataSources.filter(removeAliases).length); } @@ -55,7 +61,7 @@ export const EmptyPrompts: FC = ({ return () => { isMounted = false; }; - }, [http, hasDataIndices]); + }, [http, hasDataIndices, searchClient]); if (!hasExistingIndexPatterns && !goToForm) { if (!hasDataIndices && !remoteClustersExist) { diff --git a/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx b/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx index cabff9bfb009b..4f6f7708d90c0 100644 --- a/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx @@ -70,7 +70,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ }: Props) => { const isMounted = useRef(false); const { - services: { http, indexPatternService, uiSettings }, + services: { http, indexPatternService, uiSettings, searchClient }, } = useKibana(); const { form } = useForm({ @@ -128,13 +128,19 @@ const IndexPatternEditorFlyoutContentComponent = ({ // load all data sources and set initial matchedIndices const loadSources = useCallback(() => { - getIndices(http, () => false, '*', allowHidden).then((dataSources) => { + getIndices({ + http, + isRollupIndex: () => false, + pattern: '*', + showAllIndices: allowHidden, + searchClient, + }).then((dataSources) => { setAllSources(dataSources); const matchedSet = getMatchedIndices(dataSources, [], [], allowHidden); setMatchedIndices(matchedSet); setIsLoadingSources(false); }); - }, [http, allowHidden]); + }, [http, allowHidden, searchClient]); // loading list of index patterns useEffect(() => { @@ -223,13 +229,31 @@ const IndexPatternEditorFlyoutContentComponent = ({ const indexRequests = []; if (query?.endsWith('*')) { - const exactMatchedQuery = getIndices(http, isRollupIndex, query, allowHidden); + const exactMatchedQuery = getIndices({ + http, + isRollupIndex, + pattern: query, + showAllIndices: allowHidden, + searchClient, + }); indexRequests.push(exactMatchedQuery); // provide default value when not making a request for the partialMatchQuery indexRequests.push(Promise.resolve([])); } else { - const exactMatchQuery = getIndices(http, isRollupIndex, query, allowHidden); - const partialMatchQuery = getIndices(http, isRollupIndex, `${query}*`, allowHidden); + const exactMatchQuery = getIndices({ + http, + isRollupIndex, + pattern: query, + showAllIndices: allowHidden, + searchClient, + }); + const partialMatchQuery = getIndices({ + http, + isRollupIndex, + pattern: `${query}*`, + showAllIndices: allowHidden, + searchClient, + }); indexRequests.push(exactMatchQuery); indexRequests.push(partialMatchQuery); @@ -264,7 +288,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ return fetchIndices(newTitle); }, - [http, allowHidden, allSources, type, rollupIndicesCapabilities] + [http, allowHidden, allSources, type, rollupIndicesCapabilities, searchClient] ); useEffect(() => { diff --git a/src/plugins/index_pattern_editor/public/constants.ts b/src/plugins/index_pattern_editor/public/constants.ts index ff74e0827fa50..8d325184353df 100644 --- a/src/plugins/index_pattern_editor/public/constants.ts +++ b/src/plugins/index_pattern_editor/public/constants.ts @@ -9,3 +9,10 @@ export const pluginName = 'index_pattern_editor'; export const MAX_NUMBER_OF_MATCHING_INDICES = 100; export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns'; + +// This isn't ideal. We want to avoid searching for 20 indices +// then filtering out the majority of them because they are system indices. +// We'd like to filter system indices out in the query +// so if we can accomplish that in the future, this logic can go away +export const ESTIMATED_NUMBER_OF_SYSTEM_INDICES = 100; +export const MAX_SEARCH_SIZE = MAX_NUMBER_OF_MATCHING_INDICES + ESTIMATED_NUMBER_OF_SYSTEM_INDICES; diff --git a/src/plugins/index_pattern_editor/public/lib/get_indices.test.ts b/src/plugins/index_pattern_editor/public/lib/get_indices.test.ts index fc96482f0379f..d65cd27e090bb 100644 --- a/src/plugins/index_pattern_editor/public/lib/get_indices.test.ts +++ b/src/plugins/index_pattern_editor/public/lib/get_indices.test.ts @@ -6,11 +6,17 @@ * Side Public License, v 1. */ -import { getIndices, responseToItemArray } from './get_indices'; +import { + getIndices, + getIndicesViaSearch, + responseToItemArray, + dedupeMatchedItems, +} from './get_indices'; import { httpServiceMock } from '../../../../core/public/mocks'; -import { ResolveIndexResponseItemIndexAttrs } from '../types'; +import { ResolveIndexResponseItemIndexAttrs, MatchedItem } from '../types'; +import { Observable } from 'rxjs'; -export const successfulResponse = { +export const successfulResolveResponse = { indices: [ { name: 'remoteCluster1:bar-01', @@ -32,28 +38,99 @@ export const successfulResponse = { ], }; -const mockGetTags = () => []; -const mockIsRollupIndex = () => false; +const successfulSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + aggregations: { + indices: { + buckets: [{ key: 'kibana_sample_data_ecommerce' }, { key: '.kibana_1' }], + }, + }, + }, +}; + +const partialSearchResponse = { + isPartial: true, + isRunning: true, + rawResponse: { + hits: { + total: 2, + hits: [], + }, + }, +}; + +const errorSearchResponse = { + isPartial: true, + isRunning: false, +}; + +const isRollupIndex = () => false; +const getTags = () => []; +const searchClient = () => + new Observable((observer) => { + observer.next(successfulSearchResponse); + observer.complete(); + }) as any; const http = httpServiceMock.createStartContract(); -http.get.mockResolvedValue(successfulResponse); +http.get.mockResolvedValue(successfulResolveResponse); describe('getIndices', () => { it('should work in a basic case', async () => { - const result = await getIndices(http, mockIsRollupIndex, 'kibana', false); + const uncalledSearchClient = jest.fn(); + const result = await getIndices({ + http, + pattern: 'kibana', + searchClient: uncalledSearchClient, + isRollupIndex, + }); + expect(http.get).toHaveBeenCalled(); + expect(uncalledSearchClient).not.toHaveBeenCalled(); expect(result.length).toBe(3); expect(result[0].name).toBe('f-alias'); expect(result[1].name).toBe('foo'); }); + it('should make two calls in cross cluser case', async () => { + http.get.mockResolvedValue(successfulResolveResponse); + const result = await getIndices({ http, pattern: '*:kibana', searchClient, isRollupIndex }); + + expect(http.get).toHaveBeenCalled(); + expect(result.length).toBe(4); + expect(result[0].name).toBe('f-alias'); + expect(result[1].name).toBe('foo'); + expect(result[2].name).toBe('kibana_sample_data_ecommerce'); + expect(result[3].name).toBe('remoteCluster1:bar-01'); + }); + it('should ignore ccs query-all', async () => { - expect((await getIndices(http, mockIsRollupIndex, '*:', false)).length).toBe(0); + expect((await getIndices({ http, pattern: '*:', searchClient, isRollupIndex })).length).toBe(0); }); it('should ignore a single comma', async () => { - expect((await getIndices(http, mockIsRollupIndex, ',', false)).length).toBe(0); - expect((await getIndices(http, mockIsRollupIndex, ',*', false)).length).toBe(0); - expect((await getIndices(http, mockIsRollupIndex, ',foobar', false)).length).toBe(0); + expect((await getIndices({ http, pattern: ',', searchClient, isRollupIndex })).length).toBe(0); + expect((await getIndices({ http, pattern: ',*', searchClient, isRollupIndex })).length).toBe(0); + expect( + (await getIndices({ http, pattern: ',foobar', searchClient, isRollupIndex })).length + ).toBe(0); + }); + + it('should work with partial responses', async () => { + const searchClientPartialResponse = () => + new Observable((observer) => { + observer.next(partialSearchResponse); + observer.next(successfulSearchResponse); + observer.complete(); + }) as any; + const result = await getIndices({ + http, + pattern: '*:kibana', + searchClient: searchClientPartialResponse, + isRollupIndex, + }); + expect(result.length).toBe(4); }); it('response object to item array', () => { @@ -81,16 +158,37 @@ describe('getIndices', () => { }, ], }; - expect(responseToItemArray(result, mockGetTags)).toMatchSnapshot(); - expect(responseToItemArray({}, mockGetTags)).toEqual([]); + expect(responseToItemArray(result, getTags)).toMatchSnapshot(); + expect(responseToItemArray({}, getTags)).toEqual([]); + }); + + it('matched items are deduped', () => { + const setA = [{ name: 'a' }, { name: 'b' }] as MatchedItem[]; + const setB = [{ name: 'b' }, { name: 'c' }] as MatchedItem[]; + expect(dedupeMatchedItems(setA, setB)).toHaveLength(3); }); describe('errors', () => { - it('should handle errors gracefully', async () => { + it('should handle thrown errors gracefully', async () => { http.get.mockImplementationOnce(() => { throw new Error('Test error'); }); - const result = await getIndices(http, mockIsRollupIndex, 'kibana', false); + const result = await getIndices({ http, pattern: 'kibana', searchClient, isRollupIndex }); + expect(result.length).toBe(0); + }); + + it('getIndicesViaSearch should handle error responses gracefully', async () => { + const searchClientErrorResponse = () => + new Observable((observer) => { + observer.next(errorSearchResponse); + observer.complete(); + }) as any; + const result = await getIndicesViaSearch({ + pattern: '*:kibana', + searchClient: searchClientErrorResponse, + showAllIndices: false, + isRollupIndex, + }); expect(result.length).toBe(0); }); }); diff --git a/src/plugins/index_pattern_editor/public/lib/get_indices.ts b/src/plugins/index_pattern_editor/public/lib/get_indices.ts index 625e99ecbcdc5..8d642174232ac 100644 --- a/src/plugins/index_pattern_editor/public/lib/get_indices.ts +++ b/src/plugins/index_pattern_editor/public/lib/get_indices.ts @@ -8,10 +8,18 @@ import { sortBy } from 'lodash'; import { HttpStart } from 'kibana/public'; +import { map, filter } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { Tag, INDEX_PATTERN_TYPE } from '../types'; -// todo move into this plugin, consider removing all ipm references import { MatchedItem, ResolveIndexResponse, ResolveIndexResponseItemIndexAttrs } from '../types'; +import { MAX_SEARCH_SIZE } from '../constants'; + +import { + DataPublicPluginStart, + IEsSearchResponse, + isErrorResponse, + isCompleteResponse, +} from '../../../data/public'; const aliasLabel = i18n.translate('indexPatternEditor.aliasLabel', { defaultMessage: 'Alias' }); const dataStreamLabel = i18n.translate('indexPatternEditor.dataStreamLabel', { @@ -41,13 +49,137 @@ const getIndexTags = (isRollupIndex: (indexName: string) => boolean) => (indexNa ] : []; -export async function getIndices( - http: HttpStart, - isRollupIndex: (indexName: string) => boolean, - rawPattern: string, +export const searchResponseToArray = ( + getTags: (indexName: string) => Tag[], showAllIndices: boolean -): Promise { +) => (response: IEsSearchResponse) => { + const { rawResponse } = response; + if (!rawResponse.aggregations) { + return []; + } else { + // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response + return rawResponse.aggregations.indices.buckets + .map((bucket: { key: string }) => { + return bucket.key; + }) + .filter((indexName: string) => { + if (showAllIndices) { + return true; + } else { + return !indexName.startsWith('.'); + } + }) + .map((indexName: string) => { + return { + name: indexName, + tags: getTags(indexName), + item: {}, + }; + }); + } +}; + +export const getIndicesViaSearch = async ({ + pattern, + searchClient, + showAllIndices, + isRollupIndex, +}: { + pattern: string; + searchClient: DataPublicPluginStart['search']['search']; + showAllIndices: boolean; + isRollupIndex: (indexName: string) => boolean; +}): Promise => + searchClient({ + params: { + ignoreUnavailable: true, + expand_wildcards: showAllIndices ? 'all' : 'open', + index: pattern, + body: { + size: 0, // no hits + aggs: { + indices: { + terms: { + field: '_index', + size: MAX_SEARCH_SIZE, + }, + }, + }, + }, + }, + }) + .pipe( + filter((resp) => isCompleteResponse(resp) || isErrorResponse(resp)), + map(searchResponseToArray(getIndexTags(isRollupIndex), showAllIndices)) + ) + .toPromise() + .catch(() => []); + +export const getIndicesViaResolve = async ({ + http, + pattern, + showAllIndices, + isRollupIndex, +}: { + http: HttpStart; + pattern: string; + showAllIndices: boolean; + isRollupIndex: (indexName: string) => boolean; +}) => + http + .get(`/internal/index-pattern-management/resolve_index/${pattern}`, { + query: showAllIndices ? { expand_wildcards: 'all' } : undefined, + }) + .then((response) => { + if (!response) { + return []; + } else { + return responseToItemArray(response, getIndexTags(isRollupIndex)); + } + }); + +/** + * Takes two MatchedItem[]s and returns a merged set, with the second set prrioritized over the first based on name + * + * @param matchedA + * @param matchedB + */ + +export const dedupeMatchedItems = (matchedA: MatchedItem[], matchedB: MatchedItem[]) => { + const mergedMatchedItems = matchedA.reduce((col, item) => { + col[item.name] = item; + return col; + }, {} as Record); + + matchedB.reduce((col, item) => { + col[item.name] = item; + return col; + }, mergedMatchedItems); + + return Object.values(mergedMatchedItems).sort((a, b) => { + if (a.name > b.name) return 1; + if (b.name > a.name) return -1; + + return 0; + }); +}; + +export async function getIndices({ + http, + pattern: rawPattern = '', + showAllIndices = false, + searchClient, + isRollupIndex, +}: { + http: HttpStart; + pattern: string; + showAllIndices?: boolean; + searchClient: DataPublicPluginStart['search']['search']; + isRollupIndex: (indexName: string) => boolean; +}): Promise { const pattern = rawPattern.trim(); + const isCCS = pattern.indexOf(':') !== -1; + const requests: Array> = []; // Searching for `*:` fails for CCS environments. The search request // is worthless anyways as the we should only send a request @@ -67,20 +199,32 @@ export async function getIndices( return []; } - const query = showAllIndices ? { expand_wildcards: 'all' } : undefined; + const promiseResolve = getIndicesViaResolve({ + http, + pattern, + showAllIndices, + isRollupIndex, + }).catch(() => []); + requests.push(promiseResolve); - try { - const response = await http.get( - `/internal/index-pattern-management/resolve_index/${pattern}`, - { query } - ); - if (!response) { - return []; - } + if (isCCS) { + // CCS supports ±1 major version. We won't be able to expect resolve endpoint to exist until v9 + const promiseSearch = getIndicesViaSearch({ + pattern, + searchClient, + showAllIndices, + isRollupIndex, + }).catch(() => []); + requests.push(promiseSearch); + } - return responseToItemArray(response, getIndexTags(isRollupIndex)); - } catch { - return []; + const responses = await Promise.all(requests); + + if (responses.length === 2) { + const [resolveResponse, searchResponse] = responses; + return dedupeMatchedItems(searchResponse, resolveResponse); + } else { + return responses[0]; } } diff --git a/src/plugins/index_pattern_editor/public/open_editor.tsx b/src/plugins/index_pattern_editor/public/open_editor.tsx index ec62a1d6ec7c6..afeaff11f7403 100644 --- a/src/plugins/index_pattern_editor/public/open_editor.tsx +++ b/src/plugins/index_pattern_editor/public/open_editor.tsx @@ -23,9 +23,10 @@ import { IndexPatternEditorLazy } from './components/index_pattern_editor_lazy'; interface Dependencies { core: CoreStart; indexPatternService: DataPublicPluginStart['indexPatterns']; + searchClient: DataPublicPluginStart['search']['search']; } -export const getEditorOpener = ({ core, indexPatternService }: Dependencies) => ( +export const getEditorOpener = ({ core, indexPatternService, searchClient }: Dependencies) => ( options: IndexPatternEditorProps ): CloseEditor => { const { uiSettings, overlays, docLinks, notifications, http, application } = core; @@ -38,6 +39,7 @@ export const getEditorOpener = ({ core, indexPatternService }: Dependencies) => notifications, application, indexPatternService, + searchClient, }); let overlayRef: OverlayRef | null = null; diff --git a/src/plugins/index_pattern_editor/public/plugin.tsx b/src/plugins/index_pattern_editor/public/plugin.tsx index ca72249496e77..246386c5800e4 100644 --- a/src/plugins/index_pattern_editor/public/plugin.tsx +++ b/src/plugins/index_pattern_editor/public/plugin.tsx @@ -38,6 +38,7 @@ export class IndexPatternEditorPlugin openEditor: getEditorOpener({ core, indexPatternService: data.indexPatterns, + searchClient: data.search.search, }), /** * Index pattern editor flyout via react component @@ -53,6 +54,7 @@ export class IndexPatternEditorPlugin notifications, application, indexPatternService: data.indexPatterns, + searchClient: data.search.search, }} {...props} /> diff --git a/src/plugins/index_pattern_editor/public/types.ts b/src/plugins/index_pattern_editor/public/types.ts index 2a2abe249b330..8cc1779a804ba 100644 --- a/src/plugins/index_pattern_editor/public/types.ts +++ b/src/plugins/index_pattern_editor/public/types.ts @@ -27,6 +27,7 @@ export interface IndexPatternEditorContext { notifications: NotificationsStart; application: ApplicationStart; indexPatternService: DataPublicPluginStart['indexPatterns']; + searchClient: DataPublicPluginStart['search']['search']; } /** @public */ diff --git a/src/plugins/index_pattern_management/public/components/utils.ts b/src/plugins/index_pattern_management/public/components/utils.ts index 6520de95028c6..1273a1073fbbf 100644 --- a/src/plugins/index_pattern_management/public/components/utils.ts +++ b/src/plugins/index_pattern_management/public/components/utils.ts @@ -7,7 +7,7 @@ */ import { IndexPatternsContract } from 'src/plugins/data/public'; -import { IndexPattern, IFieldType } from 'src/plugins/data/public'; +import { IFieldType, IndexPattern, IndexPatternListItem } from 'src/plugins/data/public'; import { i18n } from '@kbn/i18n'; const defaultIndexPatternListName = i18n.translate( @@ -24,8 +24,8 @@ const rollupIndexPatternListName = i18n.translate( } ); -const isRollup = (indexPattern: IndexPattern) => { - return indexPattern.type === 'rollup'; +const isRollup = (indexPatternType: string = '') => { + return indexPatternType === 'rollup'; }; export async function getIndexPatterns( @@ -33,24 +33,22 @@ export async function getIndexPatterns( indexPatternsService: IndexPatternsContract ) { const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(true); - const indexPatternsListItems = await Promise.all( - existingIndexPatterns.map(async ({ id, title }) => { - const isDefault = defaultIndex === id; - const pattern = await indexPatternsService.get(id); - const tags = getTags(pattern, isDefault); + const indexPatternsListItems = existingIndexPatterns.map((idxPattern) => { + const { id, title } = idxPattern; + const isDefault = defaultIndex === id; + const tags = getTags(idxPattern, isDefault); - return { - id, - title, - default: isDefault, - tags, - // the prepending of 0 at the default pattern takes care of prioritization - // so the sorting will but the default index on top - // or on bottom of a the table - sort: `${isDefault ? '0' : '1'}${title}`, - }; - }) - ); + return { + id, + title, + default: isDefault, + tags, + // the prepending of 0 at the default pattern takes care of prioritization + // so the sorting will but the default index on top + // or on bottom of a the table + sort: `${isDefault ? '0' : '1'}${title}`, + }; + }); return ( indexPatternsListItems.sort((a, b) => { @@ -65,7 +63,7 @@ export async function getIndexPatterns( ); } -export const getTags = (indexPattern: IndexPattern, isDefault: boolean) => { +export const getTags = (indexPattern: IndexPatternListItem | IndexPattern, isDefault: boolean) => { const tags = []; if (isDefault) { tags.push({ @@ -73,7 +71,7 @@ export const getTags = (indexPattern: IndexPattern, isDefault: boolean) => { name: defaultIndexPatternListName, }); } - if (isRollup(indexPattern)) { + if (isRollup(indexPattern.type)) { tags.push({ key: 'rollup', name: rollupIndexPatternListName, @@ -82,17 +80,21 @@ export const getTags = (indexPattern: IndexPattern, isDefault: boolean) => { return tags; }; -export const areScriptedFieldsEnabled = (indexPattern: IndexPattern) => { - return !isRollup(indexPattern); +export const areScriptedFieldsEnabled = (indexPattern: IndexPatternListItem | IndexPattern) => { + return !isRollup(indexPattern.type); }; -export const getFieldInfo = (indexPattern: IndexPattern, field: IFieldType) => { - if (!isRollup(indexPattern)) { +export const getFieldInfo = ( + indexPattern: IndexPatternListItem | IndexPattern, + field: IFieldType +) => { + if (!isRollup(indexPattern.type)) { return []; } - const allAggs = indexPattern.typeMeta && indexPattern.typeMeta.aggs; - const fieldAggs = allAggs && Object.keys(allAggs).filter((agg) => allAggs[agg][field.name]); + const allAggs = indexPattern.typeMeta?.aggs; + const fieldAggs: string[] | undefined = + allAggs && Object.keys(allAggs).filter((agg) => allAggs[agg][field.name]); if (!fieldAggs || !fieldAggs.length) { return []; diff --git a/src/plugins/inspector/kibana.json b/src/plugins/inspector/kibana.json index 90e5d60250728..66c6617924a7e 100644 --- a/src/plugins/inspector/kibana.json +++ b/src/plugins/inspector/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "extraPublicDirs": ["common", "common/adapters/request"], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 400eca0ce418b..b6d486a656860 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; import { RedirectAppLinks, useKibana, @@ -73,7 +74,9 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => const manageDataFeatures = getFeaturesByCategory(FeatureCatalogueCategory.ADMIN); const devTools = findFeatureById('console'); const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = { - solution: 'Analytics', + solution: i18n.translate('kibanaOverview.noDataConfig.solutionName', { + defaultMessage: `Analytics`, + }), logo: 'logoKibana', actions: { beats: { diff --git a/src/plugins/kibana_react/kibana.json b/src/plugins/kibana_react/kibana.json index 6bf7ff1d82070..210b15897cfad 100644 --- a/src/plugins/kibana_react/kibana.json +++ b/src/plugins/kibana_react/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "ui": true, "server": false, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "extraPublicDirs": ["common"] } diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap b/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap index 8541558eb4cea..0ac76baa5c3e2 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap +++ b/src/plugins/kibana_react/public/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap @@ -2,6 +2,11 @@ exports[`is rendered 1`] = `
diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx index 750c45f2dfa00..430eae9f1234f 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx +++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx @@ -11,9 +11,16 @@ import sinon from 'sinon'; import { ExitFullScreenButton } from './exit_full_screen_button'; import { keys } from '@elastic/eui'; import { mount } from 'enzyme'; +import type { ChromeStart } from '../../../../core/public'; + +const MockChrome = ({ + setIsVisible: () => {}, +} as unknown) as ChromeStart; test('is rendered', () => { - const component = mount( {}} />); + const component = mount( + {}} chrome={MockChrome} /> + ); expect(component).toMatchSnapshot(); }); @@ -22,7 +29,9 @@ describe('onExitFullScreenMode', () => { test('is called when the button is pressed', () => { const onExitHandler = sinon.stub(); - const component = mount(); + const component = mount( + + ); component.find('button').simulate('click'); @@ -32,7 +41,7 @@ describe('onExitFullScreenMode', () => { test('is called when the ESC key is pressed', () => { const onExitHandler = sinon.stub(); - mount(); + mount(); const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE } as any); document.dispatchEvent(escapeKeyEvent); diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx index 3389c32de318f..9b82757523a83 100644 --- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx +++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx @@ -10,9 +10,11 @@ import { i18n } from '@kbn/i18n'; import React, { PureComponent } from 'react'; import { EuiScreenReaderOnly, keys } from '@elastic/eui'; import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import type { ChromeStart } from '../../../../core/public'; export interface ExitFullScreenButtonProps { onExitFullScreenMode: () => void; + chrome: ChromeStart; } import './index.scss'; @@ -24,11 +26,13 @@ class ExitFullScreenButtonUi extends PureComponent { } }; - public UNSAFE_componentWillMount() { + public componentDidMount() { + this.props.chrome.setIsVisible(false); document.addEventListener('keydown', this.onKeyDown, false); } public componentWillUnmount() { + this.props.chrome.setIsVisible(true); document.removeEventListener('keydown', this.onKeyDown, false); } diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap index c8fda1d036439..3f72ae5597a98 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -2,33 +2,27 @@ exports[`ElasticAgentCard props button 1`] = ` Button } - href="app/integrations/browse" + href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add a Solution integration" + title="Add Elastic Agent" /> `; exports[`ElasticAgentCard props href 1`] = ` Button @@ -36,46 +30,41 @@ exports[`ElasticAgentCard props href 1`] = ` href="#" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add a Solution integration" + title="Add Elastic Agent" /> `; exports[`ElasticAgentCard props recommended 1`] = ` - Find an integration for Solution + Add Elastic Agent } - href="app/integrations/browse" + href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add a Solution integration" + title="Add Elastic Agent" /> `; exports[`ElasticAgentCard renders 1`] = ` - Find an integration for Solution + Add Elastic Agent } - href="app/integrations/browse" + href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add a Solution integration" + title="Add Elastic Agent" /> `; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap index 1146e4f676eb6..af26f9e93ebac 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap @@ -2,31 +2,27 @@ exports[`ElasticBeatsCard props button 1`] = ` Button } - href="app/home#/tutorial" + href="/app/home#/tutorial_directory" image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" paddingSize="l" - title="Add data with Beats" + title="Add data" /> `; exports[`ElasticBeatsCard props href 1`] = ` Button @@ -34,45 +30,41 @@ exports[`ElasticBeatsCard props href 1`] = ` href="#" image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" paddingSize="l" - title="Add data with Beats" + title="Add data" /> `; exports[`ElasticBeatsCard props recommended 1`] = ` - Install Beats for Solution + Add data } - href="app/home#/tutorial" + href="/app/home#/tutorial_directory" image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" paddingSize="l" - title="Add data with Beats" + title="Add data" /> `; exports[`ElasticBeatsCard renders 1`] = ` - Install Beats for Solution + Add data } - href="app/home#/tutorial" + href="/app/home#/tutorial_directory" image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" paddingSize="l" - title="Add data with Beats" + title="Add data" /> `; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap index a8232c209ed73..fccbbe3a9e8ee 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap @@ -75,10 +75,9 @@ exports[`NoDataCard props href 1`] = `
`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx index f0ee2fc2739d9..ad3fb0e2e6396 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -/* eslint-disable @elastic/eui/href-or-on-click */ - import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; @@ -24,8 +22,9 @@ export type ElasticAgentCardProps = NoDataPageActions & { */ export const ElasticAgentCard: FunctionComponent = ({ solution, - recommended = true, - href = 'app/integrations/browse', + recommended, + title, + href, button, ...cardRest }) => { @@ -35,30 +34,26 @@ export const ElasticAgentCard: FunctionComponent = ({ const addBasePath = http.basePath.prepend; const basePathUrl = '/plugins/kibanaReact/assets/'; + const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticAgentCard.title', { + defaultMessage: 'Add Elastic Agent', + }); + const footer = typeof button !== 'string' && typeof button !== 'undefined' ? ( button ) : ( - - {button || - i18n.translate('kibana-react.noDataPage.elasticAgentCard.buttonLabel', { - defaultMessage: 'Find an integration for {solution}', - values: { solution }, - })} - + // The href and/or onClick are attached to the whole Card, so the button is just for show. + // Do not add the behavior here too or else it will propogate through + {button || title || defaultCTAtitle} ); return ( = ({ recommended, - href = 'app/home#/tutorial', + title, button, - solution, + href, + solution, // unused for now ...cardRest }) => { const { @@ -33,29 +32,26 @@ export const ElasticBeatsCard: FunctionComponent = ({ const basePathUrl = '/plugins/kibanaReact/assets/'; const IS_DARK_THEME = uiSettings.get('theme:darkMode'); + const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticBeatsCard.title', { + defaultMessage: 'Add data', + }); + const footer = typeof button !== 'string' && typeof button !== 'undefined' ? ( button ) : ( - - {button || - i18n.translate('kibana-react.noDataPage.elasticBeatsCard.buttonLabel', { - defaultMessage: 'Install Beats for {solution}', - values: { solution }, - })} - + // The href and/or onClick are attached to the whole Card, so the button is just for show. + // Do not add the behavior here too or else it will propogate through + {button || title || defaultCTAtitle} ); return ( = ({ recommended, + title, button, ...cardRest }) => { const footer = - typeof button !== 'string' ? ( - button - ) : ( - - {button} - - ); + typeof button !== 'string' ? button : {button || title}; return ( { constructor(private readonly initContext: PluginInitializerContext) {} diff --git a/src/plugins/maps_ems/kibana.json b/src/plugins/maps_ems/kibana.json index a7cf580becfd5..0807867e9dcb3 100644 --- a/src/plugins/maps_ems/kibana.json +++ b/src/plugins/maps_ems/kibana.json @@ -1,5 +1,9 @@ { "id": "mapsEms", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["map"], diff --git a/src/plugins/maps_legacy/README.md b/src/plugins/maps_legacy/README.md deleted file mode 100644 index 4a870e4f7492d..0000000000000 --- a/src/plugins/maps_legacy/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Maps legacy - -Internal objects used by the Coordinate, Region, and Vega visualizations. - -It exports the default Leaflet-based map and exposes the connection to the Elastic Maps service. - -This plugin is targeted for removal in 8.0. \ No newline at end of file diff --git a/src/plugins/maps_legacy/jest.config.js b/src/plugins/maps_legacy/jest.config.js deleted file mode 100644 index cbdcdf0905777..0000000000000 --- a/src/plugins/maps_legacy/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/maps_legacy'], -}; diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json deleted file mode 100644 index f321274791a3b..0000000000000 --- a/src/plugins/maps_legacy/kibana.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": "mapsLegacy", - "version": "8.0.0", - "kibanaVersion": "kibana", - "ui": true, - "server": true, - "requiredPlugins": ["mapsEms"], - "requiredBundles": ["visDefaultEditor", "mapsEms"] -} diff --git a/src/plugins/maps_legacy/public/common/types.ts b/src/plugins/maps_legacy/public/common/types.ts deleted file mode 100644 index 8ff1753b56c31..0000000000000 --- a/src/plugins/maps_legacy/public/common/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { TmsLayer } from '../../../maps_ems/public'; - -export interface WMSOptions { - selectedTmsLayer?: TmsLayer; - enabled: boolean; - url?: string; - options: { - version?: string; - layers?: string; - format: string; - transparent: boolean; - attribution?: string; - styles?: string; - }; -} diff --git a/src/plugins/maps_legacy/public/components/legacy_map_deprecation_message.tsx b/src/plugins/maps_legacy/public/components/legacy_map_deprecation_message.tsx deleted file mode 100644 index 513a3562c3d8f..0000000000000 --- a/src/plugins/maps_legacy/public/components/legacy_map_deprecation_message.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -interface Props { - isMapsAvailable: boolean; - onClick: (e: React.MouseEvent) => Promise; - visualizationLabel: string; -} - -export function LegacyMapDeprecationMessage(props: Props) { - const getMapsMessage = !props.isMapsAvailable ? ( - - default distribution - - ), - }} - /> - ) : null; - - const button = props.isMapsAvailable ? ( -
- - - -
- ) : null; - - return ( - -

- -

- {button} -
- ); -} diff --git a/src/plugins/maps_legacy/public/components/wms_internal_options.tsx b/src/plugins/maps_legacy/public/components/wms_internal_options.tsx deleted file mode 100644 index d666a97489b62..0000000000000 --- a/src/plugins/maps_legacy/public/components/wms_internal_options.tsx +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiLink, EuiSpacer, EuiText, EuiScreenReaderOnly } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { TextInputOption } from '../../../vis_default_editor/public'; -import { WMSOptions } from '../common/types'; - -interface WmsInternalOptions { - wms: WMSOptions; - setValue: (paramName: T, value: WMSOptions[T]) => void; -} - -function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { - const wmsLink = ( - - - - ); - const footnoteText = ( - <> - - - - ); - const footnote = ( - -

{footnoteText}

-
- ); - - const setOptions = ( - paramName: T, - value: WMSOptions['options'][T] - ) => - setValue('options', { - ...wms.options, - [paramName]: value, - }); - - return ( - <> - - - - - - - - - - - } - helpText={ - <> - - {footnote} - - } - paramName="url" - value={wms.url} - setValue={setValue} - /> - - - - - - } - helpText={ - <> - - {footnote} - - } - paramName="layers" - value={wms.options.layers} - setValue={setOptions} - /> - - - - - - } - helpText={ - <> - - {footnote} - - } - paramName="version" - value={wms.options.version} - setValue={setOptions} - /> - - - - - - } - helpText={ - <> - - {footnote} - - } - paramName="format" - value={wms.options.format} - setValue={setOptions} - /> - - - } - helpText={ - - } - paramName="attribution" - value={wms.options.attribution} - setValue={setOptions} - /> - - - - - - } - helpText={ - <> - - {footnote} - - } - paramName="styles" - value={wms.options.styles} - setValue={setOptions} - /> - - - - - - ); -} - -export { WmsInternalOptions }; diff --git a/src/plugins/maps_legacy/public/components/wms_options.tsx b/src/plugins/maps_legacy/public/components/wms_options.tsx deleted file mode 100644 index 8f63d205406e9..0000000000000 --- a/src/plugins/maps_legacy/public/components/wms_options.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useMemo } from 'react'; -import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { TmsLayer } from '../../../maps_ems/public'; -import { SelectOption, SwitchOption } from '../../../vis_default_editor/public'; -import { WmsInternalOptions } from './wms_internal_options'; -import { WMSOptions } from '../common/types'; - -interface Props { - stateParams: K; - setValue: (title: 'wms', options: WMSOptions) => void; - tmsLayers: TmsLayer[]; -} - -const mapLayerForOption = ({ id }: TmsLayer) => ({ text: id, value: id }); - -function WmsOptions({ stateParams, setValue, tmsLayers }: Props) { - const { wms } = stateParams; - const tmsLayerOptions = useMemo(() => tmsLayers.map(mapLayerForOption), [tmsLayers]); - - const setWmsOption = (paramName: T, value: WMSOptions[T]) => - setValue('wms', { - ...wms, - [paramName]: value, - }); - - const selectTmsLayer = (id: string) => { - const layer = tmsLayers.find((l: TmsLayer) => l.id === id); - if (layer) { - setWmsOption('selectedTmsLayer', layer); - } - }; - - return ( - - -

- -

-
- - - - - {!wms.enabled && ( - <> - - selectTmsLayer(value)} - /> - - )} - - {wms.enabled && } -
- ); -} - -export { WmsOptions }; diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts deleted file mode 100644 index c21aabcf743b9..0000000000000 --- a/src/plugins/maps_legacy/public/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { MapsLegacyPlugin } from './plugin'; -import * as colorUtil from './map/color_util'; -import { KibanaMapLayer } from './map/kibana_map_layer'; -import { mapTooltipProvider } from './tooltip_provider'; - -import './map/index.scss'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new MapsLegacyPlugin(initializerContext); -} - -/** @public */ -export { colorUtil, KibanaMapLayer, mapTooltipProvider }; - -export { WMSOptions } from './common/types'; -export { WmsOptions } from './components/wms_options'; -export { LegacyMapDeprecationMessage } from './components/legacy_map_deprecation_message'; - -export { lazyLoadMapsLegacyModules } from './lazy_load_bundle'; - -export type MapsLegacyPluginSetup = ReturnType; -export type MapsLegacyPluginStart = ReturnType; diff --git a/src/plugins/maps_legacy/public/kibana_services.ts b/src/plugins/maps_legacy/public/kibana_services.ts deleted file mode 100644 index 1cf02ee06db88..0000000000000 --- a/src/plugins/maps_legacy/public/kibana_services.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { IUiSettingsClient, ToastsSetup } from 'kibana/public'; -import type { MapsEmsConfig, IServiceSettings } from '../../maps_ems/public'; - -let toast: ToastsSetup; -export const setToasts = (notificationToast: ToastsSetup) => (toast = notificationToast); -export const getToasts = () => toast; - -let uiSettings: IUiSettingsClient; -export const setUiSettings = (coreUiSettings: IUiSettingsClient) => (uiSettings = coreUiSettings); -export const getUiSettings = () => uiSettings; - -let mapsEmsConfig: MapsEmsConfig; -export const setMapsEmsConfig = (config: MapsEmsConfig) => (mapsEmsConfig = config); -export const getEmsTileLayerId = () => mapsEmsConfig.emsTileLayerId; - -let getServiceSettingsFunction: () => Promise; -export const setGetServiceSettings = (getSS: () => Promise) => - (getServiceSettingsFunction = getSS); -export const getServiceSettings = async (): Promise => { - return await getServiceSettingsFunction(); -}; diff --git a/src/plugins/maps_legacy/public/lazy_load_bundle/index.ts b/src/plugins/maps_legacy/public/lazy_load_bundle/index.ts deleted file mode 100644 index b1509c4effa7a..0000000000000 --- a/src/plugins/maps_legacy/public/lazy_load_bundle/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -let loadModulesPromise: Promise; - -interface LazyLoadedMapsLegacyModules { - KibanaMap: unknown; - L: unknown; -} - -export async function lazyLoadMapsLegacyModules(): Promise { - if (typeof loadModulesPromise !== 'undefined') { - return loadModulesPromise; - } - - loadModulesPromise = new Promise(async (resolve) => { - const { KibanaMap, L } = await import('./lazy'); - - resolve({ - KibanaMap, - L, - }); - }); - return loadModulesPromise; -} diff --git a/src/plugins/maps_legacy/public/lazy_load_bundle/lazy/index.ts b/src/plugins/maps_legacy/public/lazy_load_bundle/lazy/index.ts deleted file mode 100644 index 5fb53c1a4f524..0000000000000 --- a/src/plugins/maps_legacy/public/lazy_load_bundle/lazy/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// @ts-expect-error -export { KibanaMap } from '../../map/kibana_map'; -// @ts-expect-error -export { L } from '../../leaflet'; diff --git a/src/plugins/maps_legacy/public/leaflet.js b/src/plugins/maps_legacy/public/leaflet.js deleted file mode 100644 index fd02f83d72823..0000000000000 --- a/src/plugins/maps_legacy/public/leaflet.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -if (!window.hasOwnProperty('L')) { - require('leaflet/dist/leaflet.css'); - window.L = require('leaflet/dist/leaflet.js'); - window.L.Browser.touch = false; - window.L.Browser.pointer = false; - - require('leaflet.heat/dist/leaflet-heat.js'); - require('leaflet-draw/dist/leaflet.draw.css'); - require('leaflet-draw/dist/leaflet.draw.js'); - require('leaflet-responsive-popup/leaflet.responsive.popup.css'); - require('leaflet-responsive-popup/leaflet.responsive.popup.js'); -} - -export const L = window.L; diff --git a/src/plugins/maps_legacy/public/map/_leaflet_overrides.scss b/src/plugins/maps_legacy/public/map/_leaflet_overrides.scss deleted file mode 100644 index c688a8c9b518c..0000000000000 --- a/src/plugins/maps_legacy/public/map/_leaflet_overrides.scss +++ /dev/null @@ -1,158 +0,0 @@ -// stylelint-disable selector-no-qualifying-type -// SASSTODO: Create these tooltip variables in EUI -// And/Or create a tooltip mixin -$tempEUITooltipBackground: tintOrShade($euiColorFullShade, 25%, 90%); -$tempEUITooltipText: $euiColorGhost; - -// Converted leaflet icon sprite into background svg for custom coloring (dark mode) -$visMapLeafletSprite: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 60' height='60' width='600'%3E%3Cg fill='#{hexToRGB($euiTextColor)}'%3E%3Cg%3E%3Cpath d='M18 36v6h6v-6h-6zm4 4h-2v-2h2v2z'/%3E%3Cpath d='M36 18v6h6v-6h-6zm4 4h-2v-2h2v2z'/%3E%3Cpath d='M23.142 39.145l-2.285-2.29 16-15.998 2.285 2.285z'/%3E%3C/g%3E%3Cpath d='M100 24.565l-2.096 14.83L83.07 42 76 28.773 86.463 18z'/%3E%3Cpath d='M140 20h20v20h-20z'/%3E%3Cpath d='M221 30c0 6.078-4.926 11-11 11s-11-4.922-11-11c0-6.074 4.926-11 11-11s11 4.926 11 11z'/%3E%3Cpath d='M270,19c-4.971,0-9,4.029-9,9c0,4.971,5.001,12,9,14c4.001-2,9-9.029,9-14C279,23.029,274.971,19,270,19z M270,31.5c-2.484,0-4.5-2.014-4.5-4.5c0-2.484,2.016-4.5,4.5-4.5c2.485,0,4.5,2.016,4.5,4.5C274.5,29.486,272.485,31.5,270,31.5z'/%3E%3Cg%3E%3Cpath d='M337,30.156v0.407v5.604c0,1.658-1.344,3-3,3h-10c-1.655,0-3-1.342-3-3v-10c0-1.657,1.345-3,3-3h6.345 l3.19-3.17H324c-3.313,0-6,2.687-6,6v10c0,3.313,2.687,6,6,6h10c3.314,0,6-2.687,6-6v-8.809L337,30.156'/%3E%3Cpath d='M338.72 24.637l-8.892 8.892H327V30.7l8.89-8.89z'/%3E%3Cpath d='M338.697 17.826h4v4h-4z' transform='rotate(-134.99 340.703 19.817)'/%3E%3C/g%3E%3Cg%3E%3Cpath d='M381 42h18V24h-18v18zm14-16h2v14h-2V26zm-4 0h2v14h-2V26zm-4 0h2v14h-2V26zm-4 0h2v14h-2V26z'/%3E%3Cpath d='M395 20v-4h-10v4h-6v2h22v-2h-6zm-2 0h-6v-2h6v2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A"; - -.leaflet-touch .leaflet-bar, -.leaflet-draw-actions { - @include euiBottomShadowMedium($color: $euiShadowColorLarge, $opacity: .2); - border: none; -} - -.leaflet-container { - background: $euiColorEmptyShade; - - //the heatmap layer plugin logs an error to the console when the map is in a 0-sized container - min-width: 1px !important; - min-height: 1px !important; -} - -.leaflet-clickable { - &:hover { - stroke-width: $euiSizeS; - stroke-opacity: .8; - } -} - -/** - * 1. Since Leaflet is an external library, we also have to provide EUI variables - * to non-override colors for darkmode. - */ - -.leaflet-draw-actions, -.leaflet-control { - a { - background-color: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightShade); /* 1 */ - border-color: lightOrDarkTheme($euiColorLightShade, $euiColorMediumShade) !important; /* 1 */ - color: $euiTextColor !important; /* 1 */ - - &:hover { - background-color: $euiColorLightestShade; - } - } -} - -.leaflet-touch .leaflet-bar a:first-child { - border-top-left-radius: $euiBorderRadius; - border-top-right-radius: $euiBorderRadius; -} - -.leaflet-touch .leaflet-bar a:last-child { - border-bottom-left-radius: $euiBorderRadius; - border-bottom-right-radius: $euiBorderRadius; -} - -.leaflet-retina .leaflet-draw-toolbar a { - background-image: url($visMapLeafletSprite); /* 1 */ -} - -.leaflet-control-layers-expanded { - padding: 0; - margin: 0; - @include fontSize(11px); - font-family: $euiFontFamily; - font-weight: $euiFontWeightMedium; - line-height: $euiLineHeight; - - label { - font-weight: $euiFontWeightMedium; - margin: 0; - padding: 0; - } -} - -/* over-rides leaflet popup styles to look like kibana tooltip */ -.leaflet-popup-content-wrapper { - margin: 0; - padding: 0; - background: $tempEUITooltipBackground; - color: $tempEUITooltipText; - border-radius: $euiBorderRadius !important; // Override all positions the popup might be at -} - -.leaflet-popup { - pointer-events: none; -} - -.leaflet-popup-content { - margin: 0; - @include euiFontSizeS; - font-weight: $euiFontWeightRegular; - word-wrap: break-word; - overflow: hidden; - pointer-events: none; - - > * { - margin: $euiSizeS $euiSizeS 0; - } - - > :last-child { - margin-bottom: $euiSizeS; - } - - table { - td,th { - padding: $euiSizeXS; - } - } -} - -.leaflet-popup-tip-container, -.leaflet-popup-close-button, -.leaflet-draw-tooltip { - display: none !important; -} - -.leaflet-container .leaflet-control-attribution { - background-color: transparentize($euiColorEmptyShade, .7); - color: $euiColorDarkShade; - - // attributions are appended in blocks of

tags, this will allow them to display in one line - p { - display: inline; - } -} - -.leaflet-touch .leaflet-control-zoom-in, -.leaflet-touch .leaflet-control-zoom-out { - text-indent: -10000px; - background-repeat: no-repeat; - background-position: center; -} - -// Custom SVG as background for zoom controls based off of EUI glyphs plusInCircleFilled and minusInCircleFilled -.leaflet-touch .leaflet-control-zoom-in { - background-image: url("data:image/svg+xml,%0A%3Csvg width='15px' height='15px' viewBox='0 0 15 15' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M8,7 L8,3.5 C8,3.22385763 7.77614237,3 7.5,3 C7.22385763,3 7,3.22385763 7,3.5 L7,7 L3.5,7 C3.22385763,7 3,7.22385763 3,7.5 C3,7.77614237 3.22385763,8 3.5,8 L7,8 L7,11.5 C7,11.7761424 7.22385763,12 7.5,12 C7.77614237,12 8,11.7761424 8,11.5 L8,8 L11.5,8 C11.7761424,8 12,7.77614237 12,7.5 C12,7.22385763 11.7761424,7 11.5,7 L8,7 Z M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z' /%3E%3C/svg%3E"); -} - -.leaflet-touch .leaflet-control-zoom-out { - background-image: url("data:image/svg+xml,%0A%3Csvg width='15px' height='15px' viewBox='0 0 15 15' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M7.5,0 C11.6355882,0 15,3.36441176 15,7.5 C15,11.6355882 11.6355882,15 7.5,15 C3.36441176,15 0,11.6355882 0,7.5 C0,3.36441176 3.36441176,0 7.5,0 Z M3.5,7 C3.22385763,7 3,7.22385763 3,7.5 C3,7.77614237 3.22385763,8 3.5,8 L11.5,8 C11.7761424,8 12,7.77614237 12,7.5 C12,7.22385763 11.7761424,7 11.5,7 L3.5,7 Z' /%3E%3C/svg%3E"); -} - -// Filter to desaturate mapquest tiles - -img.leaflet-tile { - @if (lightness($euiTextColor) < 50) { - filter: brightness(1.03) grayscale(.73); - } @else { - filter: invert(1) brightness(1.75) grayscale(1); - } -} - -img.leaflet-tile.filters-off { - filter: none; -} diff --git a/src/plugins/maps_legacy/public/map/_legend.scss b/src/plugins/maps_legacy/public/map/_legend.scss deleted file mode 100644 index 27016840cfabf..0000000000000 --- a/src/plugins/maps_legacy/public/map/_legend.scss +++ /dev/null @@ -1,33 +0,0 @@ -.visMapLegend { - @include fontSize(11px); - @include euiBottomShadowMedium($color: $euiShadowColorLarge); - font-family: $euiFontFamily; - font-weight: $euiFontWeightMedium; - line-height: $euiLineHeight; - color: $euiColorDarkShade; - padding: $euiSizeS; - background: transparentize($euiColorEmptyShade, .2); - border-radius: $euiBorderRadius; - - i { - @include size($euiSizeS + 2px); - display: inline-block; - margin: 3px $euiSizeXS 0 0; - border-radius: 50%; - border: 1px solid $euiColorDarkShade; - background: $euiColorDarkShade; - } -} - -.visMapLegend__title { - font-weight: $euiFontWeightBold; -} - -// Wrapper/Position - -// top left needs some more styles -.leaflet-top.leaflet-left .visMapLegend__wrapper { - position: absolute; - left: $euiSizeXXL; - white-space: nowrap; -} diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js deleted file mode 100644 index a261bcf6edd80..0000000000000 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import * as Rx from 'rxjs'; -import { filter, first } from 'rxjs/operators'; -import { - getEmsTileLayerId, - getUiSettings, - getToasts, - getServiceSettings, -} from '../kibana_services'; -import { lazyLoadMapsLegacyModules } from '../lazy_load_bundle'; - -const WMS_MINZOOM = 0; -const WMS_MAXZOOM = 22; //increase this to 22. Better for WMS - -export function BaseMapsVisualizationProvider() { - /** - * Abstract base class for a visualization consisting of a map with a single baselayer. - * @class BaseMapsVisualization - * @constructor - */ - return class BaseMapsVisualization { - constructor(element, handlers, initialVisParams) { - this.handlers = handlers; - this._params = initialVisParams; - this._container = element; - this._kibanaMap = null; - this._chartData = null; //reference to data currently on the map. - this._baseLayerDirty = true; - this._mapIsLoaded = this._makeKibanaMap(); - } - - isLoaded() { - return this._mapIsLoaded; - } - - destroy() { - if (this._kibanaMap) { - this._kibanaMap.destroy(); - this._kibanaMap = null; - } - } - - /** - * Implementation of Visualization#render. - * Child-classes can extend this method if the render-complete function requires more time until rendering has completed. - * @param esResponse - * @param status - * @return {Promise} - */ - async render(esResponse = this._esResponse, visParams = this._params) { - await this._mapIsLoaded; - - if (!this._kibanaMap) { - //the visualization has been destroyed; - return; - } - - this.resize(); - this._params = visParams; - await this._updateParams(); - - if (this._hasESResponseChanged(esResponse)) { - this._esResponse = esResponse; - await this._updateData(esResponse); - } - this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState); - - await this._whenBaseLayerIsLoaded(); - } - - resize() { - this._kibanaMap?.resize(); - } - - /** - * Creates an instance of a kibana-map with a single baselayer and assigns it to the this._kibanaMap property. - * Clients can override this method to customize the initialization. - * @private - */ - async _makeKibanaMap() { - const options = {}; - const zoomFromUiState = parseInt(this.handlers.uiState?.get('mapZoom')); - const centerFromUIState = this.handlers.uiState?.get('mapCenter'); - const { mapZoom, mapCenter } = this._getMapsParams(); - options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : mapZoom; - options.center = centerFromUIState ? centerFromUIState : mapCenter; - - const modules = await lazyLoadMapsLegacyModules(); - this._kibanaMap = new modules.KibanaMap(this._container, options); - this._kibanaMap.setMinZoom(WMS_MINZOOM); //use a default - this._kibanaMap.setMaxZoom(WMS_MAXZOOM); //use a default - - this._kibanaMap.addLegendControl(); - this._kibanaMap.addFitControl(); - this._kibanaMap.persistUiStateForVisualization(this.handlers.uiState); - - this._kibanaMap.on('baseLayer:loaded', () => { - this._baseLayerDirty = false; - }); - this._kibanaMap.on('baseLayer:loading', () => { - this._baseLayerDirty = true; - }); - await this._updateBaseLayer(); - } - - _tmsConfigured() { - const { wms } = this._getMapsParams(); - const hasTmsBaseLayer = wms && !!wms.selectedTmsLayer; - - return hasTmsBaseLayer; - } - - _wmsConfigured() { - const { wms } = this._getMapsParams(); - const hasWmsBaseLayer = wms && !!wms.enabled; - - return hasWmsBaseLayer; - } - - async _updateBaseLayer() { - const emsTileLayerId = getEmsTileLayerId(); - - if (!this._kibanaMap) { - return; - } - - const mapParams = this._getMapsParams(); - if (!this._tmsConfigured()) { - try { - const serviceSettings = await getServiceSettings(); - const tmsServices = await serviceSettings.getTMSServices(); - const userConfiguredTmsLayer = tmsServices[0]; - const initBasemapLayer = userConfiguredTmsLayer - ? userConfiguredTmsLayer - : tmsServices.find((s) => s.id === emsTileLayerId.bright); - if (initBasemapLayer) { - this._setTmsLayer(initBasemapLayer); - } - } catch (e) { - getToasts().addWarning(e.message); - return; - } - return; - } - - try { - if (this._wmsConfigured()) { - if (WMS_MINZOOM > this._kibanaMap.getMaxZoomLevel()) { - this._kibanaMap.setMinZoom(WMS_MINZOOM); - this._kibanaMap.setMaxZoom(WMS_MAXZOOM); - } - - this._kibanaMap.setBaseLayer({ - baseLayerType: 'wms', - options: { - minZoom: WMS_MINZOOM, - maxZoom: WMS_MAXZOOM, - url: mapParams.wms.url, - ...mapParams.wms.options, - }, - }); - } else if (this._tmsConfigured()) { - const selectedTmsLayer = mapParams.wms.selectedTmsLayer; - this._setTmsLayer(selectedTmsLayer); - } - } catch (tmsLoadingError) { - getToasts().addWarning(tmsLoadingError.message); - } - } - - async _setTmsLayer(tmsLayer) { - this._kibanaMap.setMinZoom(tmsLayer.minZoom); - this._kibanaMap.setMaxZoom(tmsLayer.maxZoom); - if (this._kibanaMap.getZoomLevel() > tmsLayer.maxZoom) { - this._kibanaMap.setZoomLevel(tmsLayer.maxZoom); - } - let isDesaturated = this._getMapsParams().isDesaturated; - if (typeof isDesaturated !== 'boolean') { - isDesaturated = true; - } - const isDarkMode = getUiSettings().get('theme:darkMode'); - const serviceSettings = await getServiceSettings(); - const meta = await serviceSettings.getAttributesForTMSLayer( - tmsLayer, - isDesaturated, - isDarkMode - ); - const options = { ...tmsLayer }; - delete options.id; - delete options.subdomains; - this._kibanaMap.setBaseLayer({ - baseLayerType: 'tms', - options: { ...options, ...meta }, - }); - } - - async _updateData() { - throw new Error( - i18n.translate('maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage', { - defaultMessage: 'Child should implement this method to respond to data-update', - }) - ); - } - - _hasESResponseChanged(data) { - return this._esResponse !== data; - } - - /** - * called on options change (vis.params change) - */ - async _updateParams() { - const mapParams = this._getMapsParams(); - await this._updateBaseLayer(); - this._kibanaMap.setLegendPosition(mapParams.legendPosition); - this._kibanaMap.setShowTooltip(mapParams.addTooltip); - this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState); - } - - _getMapsParams() { - return this._params; - } - - _whenBaseLayerIsLoaded() { - if (!this._tmsConfigured()) { - return true; - } - - const maxTimeForBaseLayer = 10000; - const interval$ = Rx.interval(10).pipe(filter(() => !this._baseLayerDirty)); - const timer$ = Rx.timer(maxTimeForBaseLayer); - - return Rx.race(interval$, timer$).pipe(first()).toPromise(); - } - }; -} diff --git a/src/plugins/maps_legacy/public/map/color_util.d.ts b/src/plugins/maps_legacy/public/map/color_util.d.ts deleted file mode 100644 index 8ab753e529518..0000000000000 --- a/src/plugins/maps_legacy/public/map/color_util.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function getLegendColors(colorRamp: unknown, numLegendColors?: number): string[]; - -export function getColor(colorRamp: unknown, i: number): string; diff --git a/src/plugins/maps_legacy/public/map/color_util.js b/src/plugins/maps_legacy/public/map/color_util.js deleted file mode 100644 index 64e4cedd7616c..0000000000000 --- a/src/plugins/maps_legacy/public/map/color_util.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function getLegendColors(colorRamp, numLegendColors = 4) { - const colors = []; - colors[0] = getColor(colorRamp, 0); - for (let i = 1; i < numLegendColors - 1; i++) { - colors[i] = getColor(colorRamp, Math.floor((colorRamp.length * i) / numLegendColors)); - } - colors[numLegendColors - 1] = getColor(colorRamp, colorRamp.length - 1); - return colors; -} - -export function getColor(colorRamp, i) { - const color = colorRamp[i][1]; - const red = Math.floor(color[0] * 255); - const green = Math.floor(color[1] * 255); - const blue = Math.floor(color[2] * 255); - return `rgb(${red},${green},${blue})`; -} diff --git a/src/plugins/maps_legacy/public/map/geohash_columns.test.ts b/src/plugins/maps_legacy/public/map/geohash_columns.test.ts deleted file mode 100644 index e1da8ec506d9c..0000000000000 --- a/src/plugins/maps_legacy/public/map/geohash_columns.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { geohashColumns } from './geohash_columns'; - -test('geohashColumns', () => { - expect(geohashColumns(1)).toBe(8); - expect(geohashColumns(2)).toBe(8 * 4); - expect(geohashColumns(3)).toBe(8 * 4 * 8); - expect(geohashColumns(4)).toBe(8 * 4 * 8 * 4); -}); diff --git a/src/plugins/maps_legacy/public/map/geohash_columns.ts b/src/plugins/maps_legacy/public/map/geohash_columns.ts deleted file mode 100644 index 2140b4ea3d054..0000000000000 --- a/src/plugins/maps_legacy/public/map/geohash_columns.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function geohashColumns(precision: number): number { - return geohashCells(precision, 0); -} - -/** - * Get the number of geohash cells for a given precision - * - * @param {number} precision the geohash precision (1<=precision<=12). - * @param {number} axis constant for the axis 0=lengthwise (ie. columns, along longitude), 1=heightwise (ie. rows, along latitude). - * @returns {number} Number of geohash cells (rows or columns) at that precision - */ -function geohashCells(precision: number, axis: number) { - let cells = 1; - for (let i = 1; i <= precision; i += 1) { - /* On odd precisions, rows divide by 4 and columns by 8. Vice-versa on even precisions */ - cells *= i % 2 === axis ? 4 : 8; - } - return cells; -} diff --git a/src/plugins/maps_legacy/public/map/index.scss b/src/plugins/maps_legacy/public/map/index.scss deleted file mode 100644 index f9fc841b9f868..0000000000000 --- a/src/plugins/maps_legacy/public/map/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './leaflet_overrides'; -@import './legend'; diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js deleted file mode 100644 index 62dbbda2588a5..0000000000000 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ /dev/null @@ -1,683 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EventEmitter } from 'events'; -import $ from 'jquery'; -import { get, isEqual, escape } from 'lodash'; -import { zoomToPrecision } from './zoom_to_precision'; -import { i18n } from '@kbn/i18n'; -import { ORIGIN } from '../../../maps_ems/common'; -import { L } from '../leaflet'; - -function makeFitControl(fitContainer, kibanaMap) { - // eslint-disable-next-line no-undef - const FitControl = L.Control.extend({ - options: { - position: 'topleft', - }, - initialize: function (fitContainer, kibanaMap) { - this._fitContainer = fitContainer; - this._kibanaMap = kibanaMap; - this._leafletMap = null; - }, - onAdd: function (leafletMap) { - this._leafletMap = leafletMap; - const fitDatBoundsLabel = i18n.translate( - 'maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel', - { defaultMessage: 'Fit Data Bounds' } - ); - $(this._fitContainer) - .html( - `` - ) - .on('click', (e) => { - e.preventDefault(); - this._kibanaMap.fitToData(); - }); - - return this._fitContainer; - }, - onRemove: function () { - $(this._fitContainer).off('click'); - }, - }); - - return new FitControl(fitContainer, kibanaMap); -} - -function makeLegendControl(container, kibanaMap, position) { - // eslint-disable-next-line no-undef - const LegendControl = L.Control.extend({ - options: { - position: 'topright', - }, - - initialize: function (container, kibanaMap, position) { - this._legendContainer = container; - this._kibanaMap = kibanaMap; - this.options.position = position; - }, - - updateContents() { - this._legendContainer.empty(); - const $div = $('

').addClass('visMapLegend'); - this._legendContainer.append($div); - const layers = this._kibanaMap.getLayers(); - layers.forEach((layer) => layer.appendLegendContents($div)); - }, - - onAdd: function () { - this._layerUpdateHandle = () => this.updateContents(); - this._kibanaMap.on('layers:update', this._layerUpdateHandle); - this.updateContents(); - return this._legendContainer.get(0); - }, - onRemove: function () { - this._kibanaMap.removeListener('layers:update', this._layerUpdateHandle); - this._legendContainer.empty(); - }, - }); - - return new LegendControl(container, kibanaMap, position); -} - -/** - * Collects map functionality required for Kibana. - * Serves as simple abstraction for leaflet as well. - */ -export class KibanaMap extends EventEmitter { - constructor(containerNode, options) { - super(); - this._containerNode = containerNode; - this._leafletBaseLayer = null; - this._baseLayerSettings = null; - this._baseLayerIsDesaturated = true; - - this._leafletDrawControl = null; - this._leafletFitControl = null; - this._leafletLegendControl = null; - this._legendPosition = 'topright'; - - this._layers = []; - this._listeners = []; - this._showTooltip = false; - - const leafletOptions = { - minZoom: options.minZoom, - maxZoom: options.maxZoom, - center: options.center ? options.center : [0, 0], - zoom: options.zoom ? options.zoom : 2, - // eslint-disable-next-line no-undef - renderer: L.canvas(), - zoomAnimation: false, // Desaturate map tiles causes animation rendering artifacts - zoomControl: options.zoomControl === undefined ? true : options.zoomControl, - }; - - // eslint-disable-next-line no-undef - this._leafletMap = L.map(containerNode, leafletOptions); - this._leafletMap.attributionControl.setPrefix(''); - - if (!options.scrollWheelZoom) { - this._leafletMap.scrollWheelZoom.disable(); - } - - let previousZoom = this._leafletMap.getZoom(); - this._leafletMap.on('zoomend', () => { - if (previousZoom !== this._leafletMap.getZoom()) { - previousZoom = this._leafletMap.getZoom(); - this.emit('zoomchange'); - } - }); - this._leafletMap.on('zoomend', () => this.emit('zoomend')); - this._leafletMap.on('dragend', () => this.emit('dragend')); - - this._leafletMap.on('zoomend', () => this._updateExtent()); - this._leafletMap.on('dragend', () => this._updateExtent()); - - this._leafletMap.on('mousemove', (e) => - this._layers.forEach((layer) => layer.movePointer('mousemove', e)) - ); - this._leafletMap.on('mouseout', (e) => - this._layers.forEach((layer) => layer.movePointer('mouseout', e)) - ); - this._leafletMap.on('mousedown', (e) => - this._layers.forEach((layer) => layer.movePointer('mousedown', e)) - ); - this._leafletMap.on('mouseup', (e) => - this._layers.forEach((layer) => layer.movePointer('mouseup', e)) - ); - this._leafletMap.on('draw:created', (event) => { - const drawType = event.layerType; - if (drawType === 'rectangle') { - const bounds = event.layer.getBounds(); - - const southEast = bounds.getSouthEast(); - const northWest = bounds.getNorthWest(); - let southEastLng = southEast.lng; - if (southEastLng > 180) { - southEastLng -= 360; - } - let northWestLng = northWest.lng; - if (northWestLng < -180) { - northWestLng += 360; - } - - const southEastLat = southEast.lat; - const northWestLat = northWest.lat; - - //Bounds cannot be created unless they form a box with larger than 0 dimensions - //Invalid areas are rejected by ES. - if (southEastLat === northWestLat || southEastLng === northWestLng) { - return; - } - - this.emit('drawCreated:rectangle', { - bounds: { - bottom_right: { - lat: southEastLat, - lon: southEastLng, - }, - top_left: { - lat: northWestLat, - lon: northWestLng, - }, - }, - }); - } else if (drawType === 'polygon') { - const latLongs = event.layer.getLatLngs()[0]; - this.emit('drawCreated:polygon', { - points: latLongs.map((leafletLatLng) => { - return { - lat: leafletLatLng.lat, - lon: leafletLatLng.lng, - }; - }), - }); - } - }); - - this.resize(); - } - - setShowTooltip(showTooltip) { - this._showTooltip = showTooltip; - } - - getLayers() { - return this._layers.slice(); - } - - addLayer(kibanaLayer) { - const onshowTooltip = (event) => { - if (!this._showTooltip) { - return; - } - - if (!this._popup) { - // eslint-disable-next-line no-undef - this._popup = new L.ResponsivePopup({ autoPan: false }); - this._popup.setLatLng(event.position); - this._popup.setContent(event.content); - this._leafletMap.openPopup(this._popup); - } else { - if (!this._popup.getLatLng().equals(event.position)) { - this._popup.setLatLng(event.position); - } - if (this._popup.getContent() !== event.content) { - this._popup.setContent(event.content); - } - } - }; - - kibanaLayer.on('showTooltip', onshowTooltip); - this._listeners.push({ name: 'showTooltip', handle: onshowTooltip, layer: kibanaLayer }); - - const onHideTooltip = () => { - this._leafletMap.closePopup(); - this._popup = null; - }; - kibanaLayer.on('hideTooltip', onHideTooltip); - this._listeners.push({ name: 'hideTooltip', handle: onHideTooltip, layer: kibanaLayer }); - - const onStyleChanged = () => { - if (this._leafletLegendControl) { - this._leafletLegendControl.updateContents(); - } - }; - kibanaLayer.on('styleChanged', onStyleChanged); - this._listeners.push({ name: 'styleChanged', handle: onStyleChanged, layer: kibanaLayer }); - - this._layers.push(kibanaLayer); - kibanaLayer.addToLeafletMap(this._leafletMap); - this.emit('layers:update'); - - this._addAttributions(kibanaLayer.getAttributions()); - } - - removeLayer(kibanaLayer) { - if (!kibanaLayer) { - return; - } - - this._removeAttributions(kibanaLayer.getAttributions()); - const index = this._layers.indexOf(kibanaLayer); - if (index >= 0) { - this._layers.splice(index, 1); - kibanaLayer.removeFromLeafletMap(this._leafletMap); - } - this._listeners.forEach((listener) => { - if (listener.layer === kibanaLayer) { - listener.layer.removeListener(listener.name, listener.handle); - } - }); - - //must readd all attributions, because we might have removed dupes - this._layers.forEach((layer) => this._addAttributions(layer.getAttributions())); - if (this._baseLayerSettings) { - this._addAttributions(this._baseLayerSettings.options.attribution); - } - } - - _addAttributions(attribution) { - const attributions = getAttributionArray(attribution); - attributions.forEach((attribution) => { - this._leafletMap.attributionControl.removeAttribution(attribution); //this ensures we do not add duplicates - this._leafletMap.attributionControl.addAttribution(attribution); - }); - } - - _removeAttributions(attribution) { - const attributions = getAttributionArray(attribution); - attributions.forEach((attribution) => { - this._leafletMap.attributionControl.removeAttribution(attribution); //this ensures we do not add duplicates - }); - } - - destroy() { - if (this._leafletFitControl) { - this._leafletMap.removeControl(this._leafletFitControl); - } - if (this._leafletDrawControl) { - this._leafletMap.removeControl(this._leafletDrawControl); - } - if (this._leafletLegendControl) { - this._leafletMap.removeControl(this._leafletLegendControl); - } - this.setBaseLayer(null); - let layer; - while (this._layers.length) { - layer = this._layers.pop(); - layer.removeFromLeafletMap(this._leafletMap); - } - this._leafletMap.remove(); - this._containerNode.innerHTML = ''; - this._listeners.forEach((listener) => - listener.layer.removeListener(listener.name, listener.handle) - ); - } - - getCenter() { - const center = this._leafletMap.getCenter(); - return { lon: center.lng, lat: center.lat }; - } - - setCenter(latitude, longitude) { - // eslint-disable-next-line no-undef - const latLong = L.latLng(latitude, longitude); - if (latLong.equals && !latLong.equals(this._leafletMap.getCenter())) { - this._leafletMap.setView(latLong); - } - } - - setZoomLevel(zoomLevel) { - if (this._leafletMap.getZoom() !== zoomLevel) { - this._leafletMap.setZoom(zoomLevel); - } - } - - getZoomLevel = () => { - return this._leafletMap.getZoom(); - }; - - getMaxZoomLevel = () => { - return this._leafletMap.getMaxZoom(); - }; - - getGeohashPrecision() { - return zoomToPrecision(this._leafletMap.getZoom(), 12, this._leafletMap.getMaxZoom()); - } - - getLeafletBounds() { - return this._leafletMap.getBounds(); - } - - getMetersPerPixel() { - const pointC = this._leafletMap.latLngToContainerPoint(this._leafletMap.getCenter()); // center (pixels) - const pointX = [pointC.x + 1, pointC.y]; // add one pixel to x - const pointY = [pointC.x, pointC.y + 1]; // add one pixel to y - - const latLngC = this._leafletMap.containerPointToLatLng(pointC); - const latLngX = this._leafletMap.containerPointToLatLng(pointX); - const latLngY = this._leafletMap.containerPointToLatLng(pointY); - - const distanceX = latLngC.distanceTo(latLngX); // calculate distance between c and x (latitude) - const distanceY = latLngC.distanceTo(latLngY); // calculate distance between c and y (longitude) - return Math.min(distanceX, distanceY); - } - - _getLeafletBounds(resizeOnFail) { - const boundsRaw = this._leafletMap.getBounds(); - const bounds = this._leafletMap.wrapLatLngBounds(boundsRaw); - - if (!bounds) { - return null; - } - - const southEast = bounds.getSouthEast(); - const northWest = bounds.getNorthWest(); - if (southEast.lng === northWest.lng || southEast.lat === northWest.lat) { - if (resizeOnFail) { - this._leafletMap.invalidateSize(); - return this._getLeafletBounds(false); - } else { - return null; - } - } else { - return bounds; - } - } - - getBounds() { - const bounds = this._getLeafletBounds(true); - if (!bounds) { - return null; - } - - const southEast = bounds.getSouthEast(); - const northWest = bounds.getNorthWest(); - - const southEastLng = southEast.lng; - const northWestLng = northWest.lng; - const southEastLat = southEast.lat; - const northWestLat = northWest.lat; - - // When map has not width or height, the map has no dimensions. - // These dimensions are enforced due to CSS style rules that enforce min-width/height of 0 - // that enforcement also resolves errors with the heatmap layer plugin. - - return { - bottom_right: { - lat: southEastLat, - lon: southEastLng, - }, - top_left: { - lat: northWestLat, - lon: northWestLng, - }, - }; - } - - setDesaturateBaseLayer(isDesaturated) { - if (isDesaturated === this._baseLayerIsDesaturated) { - return; - } - this._baseLayerIsDesaturated = isDesaturated; - this._updateDesaturation(); - if (this._leafletBaseLayer) { - this._leafletBaseLayer.redraw(); - } - } - - addDrawControl() { - const drawColor = '#000'; - const drawOptions = { - draw: { - polyline: false, - marker: false, - circle: false, - rectangle: { - shapeOptions: { - stroke: false, - color: drawColor, - }, - }, - polygon: { - shapeOptions: { - color: drawColor, - }, - }, - circlemarker: false, - }, - }; - // eslint-disable-next-line no-undef - this._leafletDrawControl = new L.Control.Draw(drawOptions); - this._leafletMap.addControl(this._leafletDrawControl); - } - - addFitControl() { - if (this._leafletFitControl || !this._leafletMap) { - return; - } - - // eslint-disable-next-line no-undef - const fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit'); - this._leafletFitControl = makeFitControl(fitContainer, this); - this._leafletMap.addControl(this._leafletFitControl); - } - - addLegendControl() { - if (this._leafletLegendControl || !this._leafletMap) { - return; - } - this._updateLegend(); - } - - setLegendPosition(position) { - if (this._legendPosition === position) { - if (!this._leafletLegendControl) { - this._updateLegend(); - } - } else { - this._legendPosition = position; - this._updateLegend(); - } - } - - _updateLegend() { - if (this._leafletLegendControl) { - this._leafletMap.removeControl(this._leafletLegendControl); - } - const $wrapper = $('
').addClass('visMapLegend__wrapper'); - this._leafletLegendControl = makeLegendControl($wrapper, this, this._legendPosition); - this._leafletMap.addControl(this._leafletLegendControl); - } - - resize() { - this._leafletMap.invalidateSize(); - this._updateExtent(); - } - - setMinZoom(zoom) { - this._leafletMap.setMinZoom(zoom); - } - - setMaxZoom(zoom) { - this._leafletMap.setMaxZoom(zoom); - } - - getLeafletBaseLayer() { - return this._leafletBaseLayer; - } - - setBaseLayer(settings) { - if (isEqual(settings, this._baseLayerSettings)) { - return; - } - - if (settings === null) { - if (this._leafletBaseLayer && this._leafletMap) { - this._removeAttributions(this._baseLayerSettings.options.attribution); - this._leafletMap.removeLayer(this._leafletBaseLayer); - this._leafletBaseLayer = null; - this._baseLayerSettings = null; - } - return; - } - - this._baseLayerSettings = settings; - if (this._leafletBaseLayer) { - this._leafletMap.removeLayer(this._leafletBaseLayer); - this._leafletBaseLayer = null; - } - - let baseLayer; - if (settings.baseLayerType === 'wms') { - //This is user-input that is rendered with the Leaflet attribution control. Needs to be sanitized. - this._baseLayerSettings.options.attribution = escape(settings.options.attribution); - baseLayer = this._getWMSBaseLayer(settings.options); - } else if (settings.baseLayerType === 'tms') { - baseLayer = this._getTMSBaseLayer(settings.options); - } - - if (baseLayer) { - baseLayer.on('tileload', () => this._updateDesaturation()); - baseLayer.on('load', () => { - this.emit('baseLayer:loaded'); - }); - baseLayer.on('loading', () => { - this.emit('baseLayer:loading'); - }); - - this._leafletBaseLayer = baseLayer; - this._leafletBaseLayer.addTo(this._leafletMap); - this._leafletBaseLayer.bringToBack(); - if (settings.options.minZoom > this._leafletMap.getZoom()) { - this._leafletMap.setZoom(settings.options.minZoom); - } - this._addAttributions(settings.options.attribution); - this.resize(); - } - } - - isInside(bucketRectBounds) { - const mapBounds = this._leafletMap.getBounds(); - return mapBounds.intersects(bucketRectBounds); - } - - async fitToData() { - if (!this._leafletMap) { - return; - } - - const boundsArray = await Promise.all( - this._layers.map(async (layer) => { - return await layer.getBounds(); - }) - ); - - let bounds = null; - boundsArray.forEach(async (b) => { - if (bounds) { - bounds.extend(b); - } else { - bounds = b; - } - }); - - if (bounds && bounds.isValid()) { - this._leafletMap.fitBounds(bounds); - } - } - - _getTMSBaseLayer(options) { - // eslint-disable-next-line no-undef - return L.tileLayer(options.url, { - minZoom: options.minZoom, - maxZoom: options.maxZoom, - subdomains: options.subdomains || [], - }); - } - - _getWMSBaseLayer(options) { - const wmsOptions = { - format: options.format || '', - layers: options.layers || '', - minZoom: options.minZoom, - maxZoom: options.maxZoom, - styles: options.styles || '', - transparent: options.transparent, - version: options.version || '1.3.0', - }; - - return typeof options.url === 'string' && options.url.length - ? // eslint-disable-next-line no-undef - L.tileLayer.wms(options.url, wmsOptions) - : null; - } - - _updateExtent() { - this._layers.forEach((layer) => layer.updateExtent()); - } - - _updateDesaturation() { - const tiles = $('img.leaflet-tile-loaded'); - // Don't apply client-side styling to EMS basemaps - if (get(this._baseLayerSettings, 'options.origin') === ORIGIN.EMS) { - tiles.addClass('filters-off'); - } else { - if (this._baseLayerIsDesaturated) { - tiles.removeClass('filters-off'); - } else if (!this._baseLayerIsDesaturated) { - tiles.addClass('filters-off'); - } - } - } - - persistUiStateForVisualization(uiState) { - function persistMapStateInUiState() { - const centerFromUIState = uiState.get('mapCenter'); - const zoomFromUiState = parseInt(uiState.get('mapZoom')); - - if (isNaN(zoomFromUiState) || this.getZoomLevel() !== zoomFromUiState) { - uiState.set('mapZoom', this.getZoomLevel()); - } - const centerFromMap = this.getCenter(); - if ( - !centerFromUIState || - centerFromMap.lon !== centerFromUIState[1] || - centerFromMap.lat !== centerFromUIState[0] - ) { - uiState.set('mapCenter', [centerFromMap.lat, centerFromMap.lon]); - } - } - - this.on('dragend', persistMapStateInUiState); - this.on('zoomend', persistMapStateInUiState); - } - - useUiStateFromVisualization(uiState) { - const zoomFromUiState = parseInt(uiState?.get('mapZoom')); - const centerFromUIState = uiState?.get('mapCenter'); - if (!isNaN(zoomFromUiState)) { - this.setZoomLevel(zoomFromUiState); - } - if (centerFromUIState) { - this.setCenter(centerFromUIState[0], centerFromUIState[1]); - } - } -} - -function getAttributionArray(attribution) { - const attributionString = attribution || ''; - let attributions = attributionString.split(/\s*\|\s*/); - if (attributions.length === 1) { - //temp work-around due to inconsistency in manifests of how attributions are delimited - attributions = attributions[0].split(','); - } - return attributions; -} diff --git a/src/plugins/maps_legacy/public/map/kibana_map_layer.d.ts b/src/plugins/maps_legacy/public/map/kibana_map_layer.d.ts deleted file mode 100644 index 9d3f982bd78b2..0000000000000 --- a/src/plugins/maps_legacy/public/map/kibana_map_layer.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export class KibanaMapLayer { - constructor(); - - getBounds(): Promise; - - addToLeafletMap(leafletMap: unknown): void; - - removeFromLeafletMap(leafletMap: unknown): void; - - appendLegendContents(): void; - - updateExtent(): void; - - movePointer(): void; - - getAttributions(): unknown; -} diff --git a/src/plugins/maps_legacy/public/map/kibana_map_layer.js b/src/plugins/maps_legacy/public/map/kibana_map_layer.js deleted file mode 100644 index 877ddec5f332f..0000000000000 --- a/src/plugins/maps_legacy/public/map/kibana_map_layer.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EventEmitter } from 'events'; - -export class KibanaMapLayer extends EventEmitter { - constructor() { - super(); - this._leafletLayer = null; - } - - async getBounds() { - return this._leafletLayer.getBounds(); - } - - addToLeafletMap(leafletMap) { - this._leafletLayer.addTo(leafletMap); - } - - removeFromLeafletMap(leafletMap) { - leafletMap.removeLayer(this._leafletLayer); - } - - appendLegendContents() {} - - updateExtent() {} - - movePointer() {} - - getAttributions() { - return this._attribution; - } -} diff --git a/src/plugins/maps_legacy/public/map/precision.ts b/src/plugins/maps_legacy/public/map/precision.ts deleted file mode 100644 index bf81f211e41af..0000000000000 --- a/src/plugins/maps_legacy/public/map/precision.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// @ts-ignore -import { getUiSettings } from '../kibana_services'; -import { geohashColumns } from './geohash_columns'; - -/** - * Get the number of geohash columns (world-wide) for a given precision - * @param precision the geohash precision - * @returns {number} the number of columns - */ - -const DEFAULT_PRECISION = 2; - -function getMaxPrecision() { - const config = getUiSettings(); - return parseInt(config.get('visualization:tileMap:maxPrecision'), 10) || 12; -} - -export function getZoomPrecision() { - /** - * Map Leaflet zoom levels to geohash precision levels. - * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. - */ - const zoomPrecision: any = {}; - const minGeohashPixels = 16; - const maxPrecision = getMaxPrecision(); - - for (let zoom = 0; zoom <= 21; zoom += 1) { - const worldPixels = 256 * Math.pow(2, zoom); - zoomPrecision[zoom] = 1; - for (let precision = 2; precision <= maxPrecision; precision += 1) { - const columns = geohashColumns(precision); - if (worldPixels / columns >= minGeohashPixels) { - zoomPrecision[zoom] = precision; - } else { - break; - } - } - } - return zoomPrecision; -} - -export function getPrecision(val: string) { - let precision = parseInt(val, 10); - const maxPrecision = getMaxPrecision(); - - if (Number.isNaN(precision)) { - precision = DEFAULT_PRECISION; - } - - if (precision > maxPrecision) { - return maxPrecision; - } - - return precision; -} diff --git a/src/plugins/maps_legacy/public/map/zoom_to_precision.ts b/src/plugins/maps_legacy/public/map/zoom_to_precision.ts deleted file mode 100644 index 6ff43c291e400..0000000000000 --- a/src/plugins/maps_legacy/public/map/zoom_to_precision.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { geohashColumns } from './geohash_columns'; - -const defaultMaxPrecision = 12; -const minGeoHashPixels = 16; - -const calculateZoomToPrecisionMap = (maxZoom: number): Map => { - /** - * Map Leaflet zoom levels to geohash precision levels. - * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. - */ - const zoomPrecisionMap = new Map(); - - for (let zoom = 0; zoom <= maxZoom; zoom += 1) { - if (typeof zoomPrecisionMap.get(zoom) === 'number') { - continue; - } - - const worldPixels = 256 * Math.pow(2, zoom); - - zoomPrecisionMap.set(zoom, 1); - - for (let precision = 2; precision <= defaultMaxPrecision; precision += 1) { - const columns = geohashColumns(precision); - - if (worldPixels / columns >= minGeoHashPixels) { - zoomPrecisionMap.set(zoom, precision); - } else { - break; - } - } - } - - return zoomPrecisionMap; -}; - -export function zoomToPrecision(mapZoom: number, maxPrecision: number, maxZoom: number) { - const zoomPrecisionMap = calculateZoomToPrecisionMap(typeof maxZoom === 'number' ? maxZoom : 21); - const precision = zoomPrecisionMap.get(mapZoom); - - return precision ? Math.min(precision, maxPrecision) : maxPrecision; -} diff --git a/src/plugins/maps_legacy/public/plugin.ts b/src/plugins/maps_legacy/public/plugin.ts deleted file mode 100644 index 63b3c3845f549..0000000000000 --- a/src/plugins/maps_legacy/public/plugin.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// @ts-ignore -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; -// @ts-ignore -import { - setToasts, - setUiSettings, - setMapsEmsConfig, - setGetServiceSettings, -} from './kibana_services'; -// @ts-ignore -import { getPrecision, getZoomPrecision } from './map/precision'; -import { MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index'; -import { MapsLegacyConfig } from '../config'; -// @ts-ignore -import { BaseMapsVisualizationProvider } from './map/base_maps_visualization'; -import type { MapsEmsPluginSetup } from '../../maps_ems/public'; - -/** - * These are the interfaces with your public contracts. You should export these - * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. - * @public - */ - -export interface MapsLegacySetupDependencies { - mapsEms: MapsEmsPluginSetup; -} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MapsLegacyStartDependencies {} - -export class MapsLegacyPlugin implements Plugin { - readonly _initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this._initializerContext = initializerContext; - } - - public setup(core: CoreSetup, plugins: MapsLegacySetupDependencies) { - setToasts(core.notifications.toasts); - setUiSettings(core.uiSettings); - setMapsEmsConfig(plugins.mapsEms.config); - setGetServiceSettings(plugins.mapsEms.getServiceSettings); - - const getBaseMapsVis = () => new BaseMapsVisualizationProvider(); - - return { - getZoomPrecision, - getPrecision, - getBaseMapsVis, - }; - } - - public start(core: CoreStart, plugins: MapsLegacyStartDependencies) {} -} diff --git a/src/plugins/maps_legacy/public/tooltip_provider.js b/src/plugins/maps_legacy/public/tooltip_provider.js deleted file mode 100644 index d774f19be39a8..0000000000000 --- a/src/plugins/maps_legacy/public/tooltip_provider.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import ReactDOMServer from 'react-dom/server'; - -function getToolTipContent(details) { - return ReactDOMServer.renderToStaticMarkup( - - - {details.map((detail, i) => ( - - - - - ))} - -
{detail.label}{detail.value}
- ); -} - -export function mapTooltipProvider(element, formatter) { - return (...args) => { - const details = formatter(...args); - return details && getToolTipContent(details); - }; -} diff --git a/src/plugins/maps_legacy/server/index.ts b/src/plugins/maps_legacy/server/index.ts deleted file mode 100644 index 57a7bfdd1828f..0000000000000 --- a/src/plugins/maps_legacy/server/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Plugin, PluginConfigDescriptor } from 'kibana/server'; -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { configSchema, MapsLegacyConfig } from '../config'; -import { getUiSettings } from './ui_settings'; - -export const config: PluginConfigDescriptor = { - exposeToBrowser: {}, - schema: configSchema, -}; - -export interface MapsLegacyPluginSetup { - config: MapsLegacyConfig; -} - -export class MapsLegacyPlugin implements Plugin { - readonly _initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this._initializerContext = initializerContext; - } - - public setup(core: CoreSetup) { - core.uiSettings.register(getUiSettings()); - - const pluginConfig = this._initializerContext.config.get(); - return { - config: pluginConfig, - }; - } - - public start() {} -} - -export const plugin = (initializerContext: PluginInitializerContext) => - new MapsLegacyPlugin(initializerContext); diff --git a/src/plugins/maps_legacy/server/ui_settings.ts b/src/plugins/maps_legacy/server/ui_settings.ts deleted file mode 100644 index fe516de822149..0000000000000 --- a/src/plugins/maps_legacy/server/ui_settings.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { UiSettingsParams } from 'kibana/server'; -import { schema } from '@kbn/config-schema'; - -export function getUiSettings(): Record> { - return { - 'visualization:tileMap:maxPrecision': { - name: i18n.translate('maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionTitle', { - defaultMessage: 'Maximum tile map precision', - }), - value: 7, - description: i18n.translate( - 'maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText', - { - defaultMessage: - 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, 12 is the max. {cellDimensionsLink}', - description: - 'Part of composite text: maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText + ' + - 'maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', - values: { - cellDimensionsLink: - `` + - i18n.translate( - 'maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText', - { - defaultMessage: 'Explanation of cell dimensions', - } - ) + - '', - }, - } - ), - schema: schema.number(), - category: ['visualization'], - }, - 'visualization:tileMap:WMSdefaults': { - name: i18n.translate('maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsTitle', { - defaultMessage: 'Default WMS properties', - }), - value: JSON.stringify( - { - enabled: false, - url: '', - options: { - version: '', - layers: '', - format: 'image/png', - transparent: true, - attribution: '', - styles: '', - }, - }, - null, - 2 - ), - type: 'json', - description: i18n.translate( - 'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText', - { - defaultMessage: - 'Default {propertiesLink} for the WMS map server support in the coordinate map', - description: - 'Part of composite text: maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText + ' + - 'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', - values: { - propertiesLink: - '' + - i18n.translate( - 'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText', - { - defaultMessage: 'properties', - } - ) + - '', - }, - } - ), - schema: schema.object({ - enabled: schema.boolean(), - url: schema.string(), - options: schema.object({ - version: schema.string(), - layers: schema.string(), - format: schema.string(), - transparent: schema.boolean(), - attribution: schema.string(), - styles: schema.string(), - }), - }), - category: ['visualization'], - }, - }; -} diff --git a/src/plugins/maps_legacy/tsconfig.json b/src/plugins/maps_legacy/tsconfig.json deleted file mode 100644 index b6fcb9345b1ce..0000000000000 --- a/src/plugins/maps_legacy/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["public/**/*", "server/**/*", "config.ts"], - "references": [ - { "path": "../vis_default_editor/tsconfig.json" }, - { "path": "../maps_ems/tsconfig.json" } - ] -} diff --git a/src/plugins/navigation/kibana.json b/src/plugins/navigation/kibana.json index 85d2049a34be0..aa1294847cef8 100644 --- a/src/plugins/navigation/kibana.json +++ b/src/plugins/navigation/kibana.json @@ -1,5 +1,9 @@ { "id": "navigation", + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "version": "kibana", "server": false, "ui": true, diff --git a/src/plugins/region_map/README.md b/src/plugins/region_map/README.md deleted file mode 100644 index 540ab47c102d3..0000000000000 --- a/src/plugins/region_map/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Region map visualization - -Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states. - -This plugin is targeted for removal in 8.0. \ No newline at end of file diff --git a/src/plugins/region_map/kibana.json b/src/plugins/region_map/kibana.json deleted file mode 100644 index 18ae8ec7eec8c..0000000000000 --- a/src/plugins/region_map/kibana.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "regionMap", - "version": "8.0.0", - "kibanaVersion": "kibana", - "ui": true, - "server": true, - "requiredPlugins": [ - "visualizations", - "expressions", - "mapsLegacy", - "mapsEms", - "kibanaLegacy", - "data", - "share" - ], - "requiredBundles": [ - "kibanaUtils", - "charts", - "visDefaultEditor" - ] -} diff --git a/src/plugins/region_map/package.json b/src/plugins/region_map/package.json deleted file mode 100644 index 609ab2706f9c0..0000000000000 --- a/src/plugins/region_map/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "region_map", - "version": "kibana" -} \ No newline at end of file diff --git a/src/plugins/region_map/public/__snapshots__/region_map_fn.test.ts.snap b/src/plugins/region_map/public/__snapshots__/region_map_fn.test.ts.snap deleted file mode 100644 index df72e75f5ad6b..0000000000000 --- a/src/plugins/region_map/public/__snapshots__/region_map_fn.test.ts.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`interpreter/functions#regionmap returns an object with the correct structure 1`] = ` -Object { - "as": "region_map_vis", - "type": "render", - "value": Object { - "visConfig": Object { - "addTooltip": true, - "colorSchema": "Yellow to Red", - "emsHotLink": "", - "isDisplayWarning": true, - "legendPosition": "bottomright", - "mapCenter": Array [ - 0, - 0, - ], - "mapZoom": 2, - "metric": Object { - "accessor": 0, - "aggType": "count", - "format": Object { - "id": "number", - }, - "params": Object {}, - }, - "outlineWeight": 1, - "selectedJoinField": null, - "showAllShapes": true, - "wms": Object { - "enabled": false, - "options": Object { - "format": "image/png", - "transparent": true, - }, - }, - }, - "visData": Object { - "columns": Array [ - Object { - "id": "col-0-1", - "name": "Count", - }, - ], - "rows": Array [ - Object { - "col-0-1": 0, - }, - ], - "type": "datatable", - }, - "visType": "region_map", - }, -} -`; diff --git a/src/plugins/region_map/public/choropleth_layer.js b/src/plugins/region_map/public/choropleth_layer.js deleted file mode 100644 index fe4dcb0432a13..0000000000000 --- a/src/plugins/region_map/public/choropleth_layer.js +++ /dev/null @@ -1,500 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import $ from 'jquery'; -import _ from 'lodash'; -import d3 from 'd3'; -import { i18n } from '@kbn/i18n'; -import * as topojson from 'topojson-client'; -import { getNotifications } from './kibana_services'; -import { colorUtil, KibanaMapLayer } from '../../maps_legacy/public'; -import { truncatedColorMaps } from '../../charts/public'; - -const EMPTY_STYLE = { - weight: 1, - opacity: 0.6, - color: 'rgb(200,200,200)', - fillOpacity: 0, -}; - -export class ChoroplethLayer extends KibanaMapLayer { - static _doInnerJoin(sortedMetrics, sortedGeojsonFeatures, joinField) { - let j = 0; - for (let i = 0; i < sortedGeojsonFeatures.length; i++) { - const property = sortedGeojsonFeatures[i].properties[joinField]; - sortedGeojsonFeatures[i].__kbnJoinedMetric = null; - const position = sortedMetrics.length - ? compareLexicographically(property, sortedMetrics[j].term) - : -1; - if (position === -1) { - //just need to cycle on - } else if (position === 0) { - sortedGeojsonFeatures[i].__kbnJoinedMetric = sortedMetrics[j]; - } else if (position === 1) { - //needs to catch up - while (j < sortedMetrics.length) { - const newTerm = sortedMetrics[j].term; - const newPosition = compareLexicographically(newTerm, property); - if (newPosition === -1) { - //not far enough - } else if (newPosition === 0) { - sortedGeojsonFeatures[i].__kbnJoinedMetric = sortedMetrics[j]; - break; - } else if (newPosition === 1) { - //too far! - break; - } - if (j === sortedMetrics.length - 1) { - //always keep a reference to the last metric - break; - } else { - j++; - } - } - } - } - } - - constructor( - name, - attribution, - format, - showAllShapes, - meta, - layerConfig, - serviceSettings, - leaflet - ) { - super(); - this._serviceSettings = serviceSettings; - this._metrics = null; - this._joinField = null; - this._colorRamp = truncatedColorMaps[Object.keys(truncatedColorMaps)[0]].value; - this._lineWeight = 1; - this._tooltipFormatter = () => ''; - this._attribution = attribution; - this._boundsOfData = null; - this._showAllShapes = showAllShapes; - this._layerName = name; - this._layerConfig = layerConfig; - this._leaflet = leaflet; - - // eslint-disable-next-line no-undef - this._leafletLayer = this._leaflet.geoJson(null, { - onEachFeature: (feature, layer) => { - layer.on('click', () => { - this.emit('select', feature.properties[this._joinField]); - }); - let location = null; - layer.on({ - mouseover: () => { - const tooltipContents = this._tooltipFormatter(feature); - if (!location) { - // eslint-disable-next-line no-undef - const leafletGeojson = this._leaflet.geoJson(feature); - location = leafletGeojson.getBounds().getCenter(); - } - this.emit('showTooltip', { - content: tooltipContents, - position: location, - }); - }, - mouseout: () => { - this.emit('hideTooltip'); - }, - }); - }, - style: this._makeEmptyStyleFunction(), - }); - - this._loaded = false; - this._error = false; - this._isJoinValid = false; - this._whenDataLoaded = new Promise(async (resolve) => { - try { - const data = await this._makeJsonAjaxCall(); - let featureCollection; - let formatType; - if (typeof format === 'string') { - formatType = format; - } else if (format && format.type) { - formatType = format.type; - } else { - formatType = 'geojson'; - } - - if (formatType === 'geojson') { - featureCollection = data; - } else if (formatType === 'topojson') { - const features = _.get(data, 'objects.' + meta.feature_collection_path); - featureCollection = topojson.feature(data, features); //conversion to geojson - } else { - //should never happen - throw new Error( - i18n.translate('regionMap.choroplethLayer.unrecognizedFormatErrorMessage', { - defaultMessage: 'Unrecognized format {formatType}', - values: { formatType }, - }) - ); - } - this._sortedFeatures = featureCollection.features.slice(); - this._sortFeatures(); - - if (showAllShapes) { - this._leafletLayer.addData(featureCollection); - } else { - //we need to delay adding the data until we have performed the join and know which features - //should be displayed - } - this._loaded = true; - this._setStyle(); - resolve(); - } catch (e) { - this._loaded = true; - this._error = true; - - let errorMessage; - if (e.status === 404) { - errorMessage = i18n.translate( - 'regionMap.choroplethLayer.downloadingVectorData404ErrorMessage', - { - defaultMessage: - "Server responding with '404' when attempting to fetch {name}. \ -Make sure the file exists at that location.", - values: { name: name }, - } - ); - } else { - errorMessage = i18n.translate( - 'regionMap.choroplethLayer.downloadingVectorDataErrorMessage', - { - defaultMessage: - 'Cannot download {name} file. Please ensure the \ -CORS configuration of the server permits requests from the Kibana application on this host.', - values: { name: name }, - } - ); - } - - getNotifications().toasts.addDanger({ - title: i18n.translate( - 'regionMap.choroplethLayer.downloadingVectorDataErrorMessageTitle', - { - defaultMessage: 'Error downloading vector data', - } - ), - text: errorMessage, - }); - - resolve(); - } - }); - } - - //This method is stubbed in the tests to avoid network request during unit tests. - async _makeJsonAjaxCall() { - return this._serviceSettings.getJsonForRegionLayer(this._layerConfig); - } - - _invalidateJoin() { - this._isJoinValid = false; - } - - _doInnerJoin() { - ChoroplethLayer._doInnerJoin(this._metrics, this._sortedFeatures, this._joinField); - this._isJoinValid = true; - } - - _setStyle() { - if (this._error || !this._loaded || !this._metrics || !this._joinField) { - return; - } - - if (!this._isJoinValid) { - this._doInnerJoin(); - if (!this._showAllShapes) { - const featureCollection = { - type: 'FeatureCollection', - features: this._sortedFeatures.filter((feature) => feature.__kbnJoinedMetric), - }; - this._leafletLayer.addData(featureCollection); - } - } - - const styler = this._makeChoroplethStyler(); - this._leafletLayer.setStyle(styler.leafletStyleFunction); - - if (this._metrics && this._metrics.length > 0) { - const { min, max } = getMinMax(this._metrics); - this._legendColors = colorUtil.getLegendColors(this._colorRamp); - const quantizeDomain = min !== max ? [min, max] : d3.scale.quantize().domain(); - this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors); - } - this._boundsOfData = styler.getLeafletBounds(); - this.emit('styleChanged', { - mismatches: styler.getMismatches(), - }); - } - - getUrl() { - return this._layerName; - } - - setTooltipFormatter(tooltipFormatter, fieldFormatter, fieldName, metricLabel) { - this._tooltipFormatter = (geojsonFeature) => { - if (!this._metrics) { - return ''; - } - const match = this._metrics.find((bucket) => { - return ( - compareLexicographically(bucket.term, geojsonFeature.properties[this._joinField]) === 0 - ); - }); - return tooltipFormatter(match, fieldFormatter, fieldName, metricLabel); - }; - } - - setJoinField(joinfield) { - if (joinfield === this._joinField) { - return; - } - this._joinField = joinfield; - this._sortFeatures(); - this._setStyle(); - } - - cloneChoroplethLayerForNewData( - name, - attribution, - format, - showAllData, - meta, - layerConfig, - serviceSettings, - leaflet - ) { - const clonedLayer = new ChoroplethLayer( - name, - attribution, - format, - showAllData, - meta, - layerConfig, - serviceSettings, - leaflet - ); - clonedLayer.setJoinField(this._joinField); - clonedLayer.setColorRamp(this._colorRamp); - clonedLayer.setLineWeight(this._lineWeight); - clonedLayer.setTooltipFormatter(this._tooltipFormatter); - if (this._metrics) { - clonedLayer.setMetrics(this._metrics, this._valueFormatter, this._metricTitle); - } - return clonedLayer; - } - - _sortFeatures() { - if (this._sortedFeatures && this._joinField) { - this._sortedFeatures.sort((a, b) => { - const termA = a.properties[this._joinField]; - const termB = b.properties[this._joinField]; - return compareLexicographically(termA, termB); - }); - this._invalidateJoin(); - } - } - - whenDataLoaded() { - return this._whenDataLoaded; - } - - setMetrics(metrics, fieldFormatter, metricTitle) { - this._metrics = metrics.slice(); - this._valueFormatter = fieldFormatter; - this._metricTitle = metricTitle; - - this._metrics.sort((a, b) => compareLexicographically(a.term, b.term)); - this._invalidateJoin(); - this._setStyle(); - } - - setColorRamp(colorRamp) { - if (_.isEqual(colorRamp, this._colorRamp)) { - return; - } - this._colorRamp = colorRamp; - this._setStyle(); - } - - setLineWeight(lineWeight) { - if (this._lineWeight === lineWeight) { - return; - } - this._lineWeight = lineWeight; - this._setStyle(); - } - - canReuseInstance(name, showAllShapes) { - return this._layerName === name && this._showAllShapes === showAllShapes; - } - - canReuseInstanceForNewMetrics(name, showAllShapes, newMetrics) { - if (this._layerName !== name) { - return false; - } - - if (showAllShapes) { - return this._showAllShapes === showAllShapes; - } - - if (!this._metrics) { - return; - } - - const currentKeys = Object.keys(this._metrics); - const newKeys = Object.keys(newMetrics); - return _.isEqual(currentKeys, newKeys); - } - - getBounds() { - const bounds = super.getBounds(); - return this._boundsOfData ? this._boundsOfData : bounds; - } - - appendLegendContents(jqueryDiv) { - if (!this._legendColors || !this._legendQuantizer) { - return; - } - - const titleText = this._metricTitle; - const $title = $('
').addClass('visMapLegend__title').text(titleText); - jqueryDiv.append($title); - - this._legendColors.forEach((color) => { - const labelText = this._legendQuantizer - .invertExtent(color) - .map((val) => { - return this._valueFormatter.convert(val); - }) - .join(' – '); - - const label = $('
'); - const icon = $('').css({ - background: color, - 'border-color': makeColorDarker(color), - }); - - const text = $('').text(labelText); - label.append(icon); - label.append(text); - - jqueryDiv.append(label); - }); - } - - _makeEmptyStyleFunction() { - const emptyStyle = _.assign({}, EMPTY_STYLE, { - weight: this._lineWeight, - }); - - return () => { - return emptyStyle; - }; - } - - _makeChoroplethStyler() { - const emptyStyle = this._makeEmptyStyleFunction(); - if (this._metrics.length === 0) { - return { - leafletStyleFunction: () => { - return emptyStyle(); - }, - getMismatches: () => { - return []; - }, - getLeafletBounds: () => { - return null; - }, - }; - } - - const { min, max } = getMinMax(this._metrics); - - // eslint-disable-next-line no-undef - const boundsOfAllFeatures = new this._leaflet.LatLngBounds(); - return { - leafletStyleFunction: (geojsonFeature) => { - const match = geojsonFeature.__kbnJoinedMetric; - if (!match) { - return emptyStyle(); - } - // eslint-disable-next-line no-undef - const boundsOfFeature = this._leaflet.geoJson(geojsonFeature).getBounds(); - boundsOfAllFeatures.extend(boundsOfFeature); - - return { - fillColor: getChoroplethColor(match.value, min, max, this._colorRamp), - weight: this._lineWeight, - opacity: 1, - color: 'white', - fillOpacity: 0.7, - }; - }, - /** - * should not be called until getLeafletStyleFunction has been called - * @return {Array} - */ - getMismatches: () => { - const mismatches = this._metrics.slice(); - this._sortedFeatures.forEach((feature) => { - const index = mismatches.indexOf(feature.__kbnJoinedMetric); - if (index >= 0) { - mismatches.splice(index, 1); - } - }); - return mismatches.map((b) => b.term); - }, - getLeafletBounds: function () { - return boundsOfAllFeatures.isValid() ? boundsOfAllFeatures : null; - }, - }; - } -} - -//lexicographic compare -function compareLexicographically(termA, termB) { - termA = typeof termA === 'string' ? termA : termA.toString(); - termB = typeof termB === 'string' ? termB : termB.toString(); - return termA.localeCompare(termB); -} - -function makeColorDarker(color) { - const amount = 1.3; //magic number, carry over from earlier - return d3.hcl(color).darker(amount).toString(); -} - -function getMinMax(data) { - let min = data[0].value; - let max = data[0].value; - for (let i = 1; i < data.length; i += 1) { - min = Math.min(data[i].value, min); - max = Math.max(data[i].value, max); - } - return { min, max }; -} - -function getChoroplethColor(value, min, max, colorRamp) { - if (min === max) { - return colorUtil.getColor(colorRamp, colorRamp.length - 1); - } - const fraction = (value - min) / (max - min); - const index = Math.round(colorRamp.length * fraction) - 1; - const i = Math.max(Math.min(colorRamp.length - 1, index), 0); - - return colorUtil.getColor(colorRamp, i); -} diff --git a/src/plugins/region_map/public/components/index.tsx b/src/plugins/region_map/public/components/index.tsx deleted file mode 100644 index 55e26ec311c41..0000000000000 --- a/src/plugins/region_map/public/components/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { lazy } from 'react'; -import { IServiceSettings } from 'src/plugins/maps_ems/public'; -import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { RegionMapVisParams } from '../region_map_types'; - -const RegionMapOptions = lazy(() => import('./region_map_options')); - -export const createRegionMapOptions = (getServiceSettings: () => Promise) => ( - props: VisEditorOptionsProps -) => ; diff --git a/src/plugins/region_map/public/components/region_map_options.tsx b/src/plugins/region_map/public/components/region_map_options.tsx deleted file mode 100644 index e3be8f9dcec09..0000000000000 --- a/src/plugins/region_map/public/components/region_map_options.tsx +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useCallback, useMemo } from 'react'; -import { EuiIcon, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { truncatedColorSchemas } from '../../../charts/public'; -import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_ems/public'; -import { SelectOption, SwitchOption, NumberInputOption } from '../../../vis_default_editor/public'; -import { WmsOptions } from '../../../maps_legacy/public'; -import { RegionMapVisParams } from '../region_map_types'; -import { getTmsLayers, getVectorLayers } from '../kibana_services'; - -const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ - text: name, - value: layerId, -}); - -const mapFieldForOption = ({ description, name }: FileLayerField) => ({ - text: description, - value: name, -}); - -const tmsLayers = getTmsLayers(); -const vectorLayers = getVectorLayers(); -const vectorLayerOptions = vectorLayers.map(mapLayerForOption); - -export type RegionMapOptionsProps = { - getServiceSettings: () => Promise; -} & VisEditorOptionsProps; - -function RegionMapOptions(props: RegionMapOptionsProps) { - const { getServiceSettings, stateParams, setValue } = props; - const fieldOptions = useMemo( - () => - ((stateParams.selectedLayer && stateParams.selectedLayer.fields) || []).map( - mapFieldForOption - ), - [stateParams.selectedLayer] - ); - - const setEmsHotLink = useCallback( - async (layer: VectorLayer) => { - const serviceSettings = await getServiceSettings(); - const emsHotLink = await serviceSettings.getEMSHotLink(layer); - setValue('emsHotLink', emsHotLink); - }, - [setValue, getServiceSettings] - ); - - const setLayer = useCallback( - async (paramName: 'selectedLayer', value: VectorLayer['layerId']) => { - const newLayer = vectorLayers.find(({ layerId }: VectorLayer) => layerId === value); - - if (newLayer) { - setValue(paramName, newLayer); - setValue('selectedJoinField', newLayer.fields[0]); - setEmsHotLink(newLayer); - } - }, - [setEmsHotLink, setValue] - ); - - const setField = useCallback( - (paramName: 'selectedJoinField', value: FileLayerField['name']) => { - if (stateParams.selectedLayer) { - setValue( - paramName, - stateParams.selectedLayer.fields.find((f) => f.name === value) - ); - } - }, - [setValue, stateParams.selectedLayer] - ); - - return ( - <> - - -

- -

-
- - - - - {' '} - - - - ) - } - options={vectorLayerOptions} - paramName="selectedLayer" - value={stateParams.selectedLayer && stateParams.selectedLayer.layerId} - setValue={setLayer} - /> - - - - - - -
- - - - - -

- -

-
- - - - - -
- - - - - - ); -} - -// default export required for React.Lazy -// eslint-disable-next-line import/no-default-export -export { RegionMapOptions as default }; diff --git a/src/plugins/region_map/public/get_deprecation_message.tsx b/src/plugins/region_map/public/get_deprecation_message.tsx deleted file mode 100644 index 2606c8ed108e2..0000000000000 --- a/src/plugins/region_map/public/get_deprecation_message.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { getQueryService, getShareService } from './kibana_services'; -import { Vis } from '../../visualizations/public'; -import { LegacyMapDeprecationMessage } from '../../maps_legacy/public'; - -function getEmsLayerId(id: string | number, layerId: string) { - if (typeof id === 'string') { - return id; - } - - // Region maps from 6.x will have numerical EMS id refering to S3 bucket id. - // In this case, use layerId with contains the EMS layer name. - const split = layerId.split('.'); - return split.length === 2 ? split[1] : undefined; -} - -export function getDeprecationMessage(vis: Vis) { - const title = i18n.translate('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }); - - async function onClick(e: React.MouseEvent) { - e.preventDefault(); - - const locator = getShareService().url.locators.get('MAPS_APP_REGION_MAP_LOCATOR'); - if (!locator) return; - - const query = getQueryService(); - const params: { [key: string]: any } = { - label: vis.title ? vis.title : title, - emsLayerId: vis.params.selectedLayer.isEMS - ? getEmsLayerId(vis.params.selectedLayer.id, vis.params.selectedLayer.layerId) - : undefined, - leftFieldName: vis.params.selectedLayer.isEMS ? vis.params.selectedJoinField.name : undefined, - colorSchema: vis.params.colorSchema, - indexPatternId: vis.data.indexPattern?.id, - indexPatternTitle: vis.data.indexPattern?.title, - metricAgg: 'count', - filters: query.filterManager.getFilters(), - query: query.queryString.getQuery(), - timeRange: query.timefilter.timefilter.getTime(), - }; - - const bucketAggs = vis.data?.aggs?.byType('buckets'); - if (bucketAggs?.length && bucketAggs[0].type.dslName === 'terms') { - params.termsFieldName = bucketAggs[0].getField()?.name; - params.termsSize = bucketAggs[0].getParam('size'); - } - - const metricAggs = vis.data?.aggs?.byType('metrics'); - if (metricAggs?.length) { - params.metricAgg = metricAggs[0].type.dslName; - params.metricFieldName = metricAggs[0].getField()?.name; - } - - locator.navigate(params); - } - - return ( - - ); -} diff --git a/src/plugins/region_map/public/index.ts b/src/plugins/region_map/public/index.ts deleted file mode 100644 index b2b0f38e6e96d..0000000000000 --- a/src/plugins/region_map/public/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { RegionMapPlugin as Plugin } from './plugin'; - -export interface RegionMapsConfigType { - includeElasticMapsService: boolean; - layers: any[]; -} - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/plugins/region_map/public/kibana_services.ts b/src/plugins/region_map/public/kibana_services.ts deleted file mode 100644 index 030d021006d98..0000000000000 --- a/src/plugins/region_map/public/kibana_services.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { CoreStart } from 'kibana/public'; -import { NotificationsStart } from 'kibana/public'; -import { createGetterSetter } from '../../kibana_utils/public'; -import { DataPublicPluginStart } from '../../data/public'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; -import { SharePluginStart } from '../../share/public'; -import { VectorLayer, TmsLayer } from '../../maps_ems/public'; - -export const [getCoreService, setCoreService] = createGetterSetter('Core'); - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('data.fieldFormats'); - -export const [getNotifications, setNotifications] = createGetterSetter( - 'Notifications' -); - -export const [getQueryService, setQueryService] = createGetterSetter< - DataPublicPluginStart['query'] ->('Query'); - -export const [getShareService, setShareService] = createGetterSetter('Share'); - -export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter( - 'KibanaLegacy' -); - -export const [getTmsLayers, setTmsLayers] = createGetterSetter('TmsLayers'); - -export const [getVectorLayers, setVectorLayers] = createGetterSetter('VectorLayers'); diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts deleted file mode 100644 index 724404849aea8..0000000000000 --- a/src/plugins/region_map/public/plugin.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, - IUiSettingsClient, - NotificationsStart, -} from 'kibana/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { VisualizationsSetup } from '../../visualizations/public'; -// @ts-ignore -import { createRegionMapFn } from './region_map_fn'; -// @ts-ignore -import { createRegionMapTypeDefinition } from './region_map_type'; -import { MapsLegacyPluginSetup } from '../../maps_legacy/public'; -import { IServiceSettings, MapsEmsPluginSetup } from '../../maps_ems/public'; -import { - setCoreService, - setFormatService, - setNotifications, - setKibanaLegacy, - setQueryService, - setShareService, -} from './kibana_services'; -import { DataPublicPluginStart } from '../../data/public'; -import { RegionMapsConfigType } from './index'; -import { MapsLegacyConfig } from '../../maps_legacy/config'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; -import { SharePluginStart } from '../../share/public'; -import { getRegionMapRenderer } from './region_map_renderer'; - -/** @private */ -export interface RegionMapVisualizationDependencies { - uiSettings: IUiSettingsClient; - regionmapsConfig: RegionMapsConfig; - getServiceSettings: () => Promise; - BaseMapsVisualization: any; -} - -/** @internal */ -export interface RegionMapPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - mapsLegacy: MapsLegacyPluginSetup; - mapsEms: MapsEmsPluginSetup; -} - -/** @internal */ -export interface RegionMapPluginStartDependencies { - data: DataPublicPluginStart; - notifications: NotificationsStart; - kibanaLegacy: KibanaLegacyStart; - share: SharePluginStart; -} - -/** @internal */ -export interface RegionMapsConfig { - includeElasticMapsService: boolean; - layers: any[]; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface RegionMapPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface RegionMapPluginStart {} - -/** @internal */ -export class RegionMapPlugin implements Plugin { - readonly _initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this._initializerContext = initializerContext; - } - - public setup( - core: CoreSetup, - { expressions, visualizations, mapsLegacy, mapsEms }: RegionMapPluginSetupDependencies - ) { - const config = { - ...this._initializerContext.config.get(), - // The maps legacy plugin updates the regionmap config directly in service_settings, - // future work on how configurations across the different plugins are organized would - // ideally constrain regionmap config updates to occur only from this plugin - ...mapsEms.config.regionmap, - }; - const visualizationDependencies: Readonly = { - uiSettings: core.uiSettings, - regionmapsConfig: config as RegionMapsConfig, - getServiceSettings: mapsEms.getServiceSettings, - BaseMapsVisualization: mapsLegacy.getBaseMapsVis(), - }; - - expressions.registerFunction(createRegionMapFn); - expressions.registerRenderer(getRegionMapRenderer(visualizationDependencies)); - - visualizations.createBaseVisualization( - createRegionMapTypeDefinition(visualizationDependencies) - ); - - return {}; - } - - public start(core: CoreStart, plugins: RegionMapPluginStartDependencies) { - setCoreService(core); - setFormatService(plugins.data.fieldFormats); - setQueryService(plugins.data.query); - setNotifications(core.notifications); - setKibanaLegacy(plugins.kibanaLegacy); - setShareService(plugins.share); - return {}; - } -} diff --git a/src/plugins/region_map/public/region_map_fn.test.ts b/src/plugins/region_map/public/region_map_fn.test.ts deleted file mode 100644 index ffb8f22f0aa1c..0000000000000 --- a/src/plugins/region_map/public/region_map_fn.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; -import { createRegionMapFn } from './region_map_fn'; - -describe('interpreter/functions#regionmap', () => { - const fn = functionWrapper(createRegionMapFn()); - const context = { - type: 'datatable', - rows: [{ 'col-0-1': 0 }], - columns: [{ id: 'col-0-1', name: 'Count' }], - }; - const visConfig = { - legendPosition: 'bottomright', - addTooltip: true, - colorSchema: 'Yellow to Red', - emsHotLink: '', - selectedJoinField: null, - isDisplayWarning: true, - wms: { - enabled: false, - options: { - format: 'image/png', - transparent: true, - }, - }, - mapZoom: 2, - mapCenter: [0, 0], - outlineWeight: 1, - showAllShapes: true, - metric: { - accessor: 0, - format: { - id: 'number', - }, - params: {}, - aggType: 'count', - }, - }; - - it('returns an object with the correct structure', () => { - const actual = fn(context, { visConfig: JSON.stringify(visConfig) }); - expect(actual).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/region_map/public/region_map_fn.ts b/src/plugins/region_map/public/region_map_fn.ts deleted file mode 100644 index df3471bc92051..0000000000000 --- a/src/plugins/region_map/public/region_map_fn.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -import type { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; -import { RegionMapVisConfig } from './region_map_types'; - -interface Arguments { - visConfig: string | null; -} - -export interface RegionMapVisRenderValue { - visData: Datatable; - visType: 'region_map'; - visConfig: RegionMapVisConfig; -} - -export type RegionMapExpressionFunctionDefinition = ExpressionFunctionDefinition< - 'regionmap', - Datatable, - Arguments, - Render ->; - -export const createRegionMapFn = (): RegionMapExpressionFunctionDefinition => ({ - name: 'regionmap', - type: 'render', - context: { - types: ['datatable'], - }, - help: i18n.translate('regionMap.function.help', { - defaultMessage: 'Regionmap visualization', - }), - args: { - visConfig: { - types: ['string', 'null'], - default: '"{}"', - help: '', - }, - }, - fn(context, args, handlers) { - const visConfig = args.visConfig && JSON.parse(args.visConfig); - - if (handlers?.inspectorAdapters?.tables) { - handlers.inspectorAdapters.tables.logDatatable('default', context); - } - return { - type: 'render', - as: 'region_map_vis', - value: { - visData: context, - visType: 'region_map', - visConfig, - }, - }; - }, -}); diff --git a/src/plugins/region_map/public/region_map_renderer.tsx b/src/plugins/region_map/public/region_map_renderer.tsx deleted file mode 100644 index e74a2013daf3b..0000000000000 --- a/src/plugins/region_map/public/region_map_renderer.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { lazy } from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { ExpressionRenderDefinition } from 'src/plugins/expressions'; -import { VisualizationContainer } from '../../visualizations/public'; -import { RegionMapVisualizationDependencies } from './plugin'; -import { RegionMapVisRenderValue } from './region_map_fn'; - -const RegionMapVisualization = lazy(() => import('./region_map_visualization_component')); - -export const getRegionMapRenderer: ( - deps: RegionMapVisualizationDependencies -) => ExpressionRenderDefinition = (deps) => ({ - name: 'region_map_vis', - reuseDomNode: true, - render: async (domNode, { visConfig, visData }, handlers) => { - handlers.onDestroy(() => { - unmountComponentAtNode(domNode); - }); - - render( - - - , - domNode - ); - }, -}); diff --git a/src/plugins/region_map/public/region_map_type.ts b/src/plugins/region_map/public/region_map_type.ts deleted file mode 100644 index 5797812be34b0..0000000000000 --- a/src/plugins/region_map/public/region_map_type.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -import { VisTypeDefinition } from '../../visualizations/public'; -import { ORIGIN, VectorLayer } from '../../maps_ems/public'; - -import { getDeprecationMessage } from './get_deprecation_message'; -import { RegionMapVisualizationDependencies } from './plugin'; -import { createRegionMapOptions } from './components'; -import { toExpressionAst } from './to_ast'; -import { RegionMapVisParams } from './region_map_types'; -import { mapToLayerWithId } from './util'; -import { setTmsLayers, setVectorLayers } from './kibana_services'; - -export function createRegionMapTypeDefinition({ - uiSettings, - regionmapsConfig, - getServiceSettings, -}: RegionMapVisualizationDependencies): VisTypeDefinition { - return { - name: 'region_map', - getInfoMessage: getDeprecationMessage, - title: i18n.translate('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }), - description: i18n.translate('regionMap.mapVis.regionMapDescription', { - defaultMessage: - 'Show metrics on a thematic map. Use one of the \ -provided base maps, or add your own. Darker colors represent higher values.', - }), - icon: 'visMapRegion', - visConfig: { - defaults: { - legendPosition: 'bottomright', - addTooltip: true, - colorSchema: 'Yellow to Red', - emsHotLink: '', - isDisplayWarning: true, - wms: uiSettings.get('visualization:tileMap:WMSdefaults'), - mapZoom: 2, - mapCenter: [0, 0], - outlineWeight: 1, - showAllShapes: true, // still under consideration - }, - }, - editorConfig: { - optionsTemplate: createRegionMapOptions(getServiceSettings), - schemas: [ - { - group: 'metrics', - name: 'metric', - title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.metricTitle', { - defaultMessage: 'Value', - }), - min: 1, - max: 1, - aggFilter: [ - 'count', - 'avg', - 'sum', - 'min', - 'max', - 'cardinality', - 'top_hits', - 'sum_bucket', - 'min_bucket', - 'max_bucket', - 'avg_bucket', - ], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: 'buckets', - name: 'segment', - title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.segmentTitle', { - defaultMessage: 'Shape field', - }), - min: 1, - max: 1, - aggFilter: ['terms'], - }, - ], - }, - toExpressionAst, - setup: async (vis) => { - const serviceSettings = await getServiceSettings(); - const tmsLayers = await serviceSettings.getTMSServices(); - setTmsLayers(tmsLayers); - setVectorLayers([]); - - if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) { - vis.params.wms.selectedTmsLayer = tmsLayers[0]; - } - - const vectorLayers = regionmapsConfig.layers.map( - mapToLayerWithId.bind(null, ORIGIN.KIBANA_YML) - ); - let selectedLayer = vectorLayers[0]; - let selectedJoinField = selectedLayer ? selectedLayer.fields[0] : undefined; - if (regionmapsConfig.includeElasticMapsService) { - const layers = await serviceSettings.getFileLayers(); - const newLayers = layers - .map(mapToLayerWithId.bind(null, ORIGIN.EMS)) - .filter( - (layer: VectorLayer) => - !vectorLayers.some((vectorLayer) => vectorLayer.layerId === layer.layerId) - ); - - // backfill v1 manifest for now - newLayers.forEach((layer: VectorLayer) => { - if (layer.format === 'geojson') { - layer.format = { - type: 'geojson', - }; - } - }); - - const allVectorLayers = [...vectorLayers, ...newLayers]; - setVectorLayers(allVectorLayers); - - [selectedLayer] = allVectorLayers; - selectedJoinField = selectedLayer ? selectedLayer.fields[0] : undefined; - - if (selectedLayer && !vis.params.selectedLayer && selectedLayer.isEMS) { - vis.params.emsHotLink = await serviceSettings.getEMSHotLink(selectedLayer); - } - } - - if (!vis.params.selectedLayer) { - vis.params.selectedLayer = selectedLayer; - vis.params.selectedJoinField = selectedJoinField; - } - - return vis; - }, - requiresSearch: true, - }; -} diff --git a/src/plugins/region_map/public/region_map_types.ts b/src/plugins/region_map/public/region_map_types.ts deleted file mode 100644 index 0a9235f1c82df..0000000000000 --- a/src/plugins/region_map/public/region_map_types.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { SchemaConfig } from 'src/plugins/visualizations/public'; -import { VectorLayer, FileLayerField } from '../../maps_ems/public'; -import { WMSOptions } from '../../maps_legacy/public'; - -export interface RegionMapVisParams { - readonly addTooltip: true; - readonly legendPosition: 'bottomright'; - colorSchema: string; - emsHotLink?: string | null; - mapCenter: [number, number]; - mapZoom: number; - outlineWeight: number | ''; - isDisplayWarning: boolean; - showAllShapes: boolean; - selectedLayer?: VectorLayer; - selectedJoinField?: FileLayerField; - wms: WMSOptions; -} - -export interface RegionMapVisConfig extends RegionMapVisParams { - metric: SchemaConfig; - bucket?: SchemaConfig; -} diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js deleted file mode 100644 index 80bee417009a4..0000000000000 --- a/src/plugins/region_map/public/region_map_visualization.js +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { getFormatService, getNotifications, getKibanaLegacy } from './kibana_services'; -import { truncatedColorMaps } from '../../charts/public'; -import { tooltipFormatter } from './tooltip_formatter'; -import { mapTooltipProvider, lazyLoadMapsLegacyModules } from '../../maps_legacy/public'; -import { ORIGIN } from '../../maps_ems/public'; - -export function createRegionMapVisualization({ - regionmapsConfig, - uiSettings, - BaseMapsVisualization, - getServiceSettings, -}) { - return class RegionMapsVisualization extends BaseMapsVisualization { - constructor(container, handlers, initialVisParams) { - super(container, handlers, initialVisParams); - this._choroplethLayer = null; - this._tooltipFormatter = mapTooltipProvider(container, tooltipFormatter); - } - - async render(esResponse, visParams) { - getKibanaLegacy().loadFontAwesome(); - await super.render(esResponse, visParams); - if (this._choroplethLayer) { - await this._choroplethLayer.whenDataLoaded(); - } - } - - async _updateData(table) { - this._chartData = table; - const termColumn = this._params.bucket ? table.columns[this._params.bucket.accessor] : null; - const valueColumn = table.columns[this._params.metric.accessor]; - let results; - if (!this._hasColumns() || !table.rows.length) { - results = []; - } else { - results = table.rows.map((row) => { - const term = row[termColumn.id]; - const value = row[valueColumn.id]; - return { term: term, value: value }; - }); - } - - const selectedLayer = await this._loadConfig(this._params.selectedLayer); - if (!this._params.selectedJoinField && selectedLayer) { - this._params.selectedJoinField = selectedLayer.fields[0]; - } - - if (!selectedLayer) { - return; - } - - await this._updateChoroplethLayerForNewMetrics( - selectedLayer.name, - selectedLayer.attribution, - this._params.showAllShapes, - results - ); - - const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format); - - this._choroplethLayer.setMetrics(results, metricFieldFormatter, valueColumn.name); - if (termColumn && valueColumn) { - this._choroplethLayer.setTooltipFormatter( - this._tooltipFormatter, - metricFieldFormatter, - termColumn.name, - valueColumn.name - ); - } - - this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState); - } - - async _loadConfig(fileLayerConfig) { - // Load the selected layer from the metadata-service. - // Do not use the selectedLayer from the visState. - // These settings are stored in the URL and can be used to inject dirty display content. - - const { escape } = await import('lodash'); - - if ( - fileLayerConfig.isEMS || //Hosted by EMS. Metadata needs to be resolved through EMS - (fileLayerConfig.layerId && fileLayerConfig.layerId.startsWith(`${ORIGIN.EMS}.`)) //fallback for older saved objects - ) { - const serviceSettings = await getServiceSettings(); - return await serviceSettings.loadFileLayerConfig(fileLayerConfig); - } - - //Configured in the kibana.yml. Needs to be resolved through the settings. - const configuredLayer = regionmapsConfig.layers.find( - (layer) => layer.name === fileLayerConfig.name - ); - - if (configuredLayer) { - return { - ...configuredLayer, - attribution: escape(configuredLayer.attribution ? configuredLayer.attribution : ''), - }; - } - - return null; - } - - async _updateParams() { - await super._updateParams(); - - const selectedLayer = await this._loadConfig(this._params.selectedLayer); - - if (!this._params.selectedJoinField && selectedLayer) { - this._params.selectedJoinField = selectedLayer.fields[0]; - } - - if (!this._params.selectedJoinField || !selectedLayer) { - return; - } - - await this._updateChoroplethLayerForNewProperties( - selectedLayer.name, - selectedLayer.attribution, - this._params.showAllShapes - ); - - const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format); - - this._choroplethLayer.setJoinField(this._params.selectedJoinField.name); - this._choroplethLayer.setColorRamp(truncatedColorMaps[this._params.colorSchema].value); - this._choroplethLayer.setLineWeight(this._params.outlineWeight); - this._choroplethLayer.setTooltipFormatter( - this._tooltipFormatter, - metricFieldFormatter, - this._metricLabel - ); - } - - async _updateChoroplethLayerForNewMetrics(name, attribution, showAllData, newMetrics) { - if ( - this._choroplethLayer && - this._choroplethLayer.canReuseInstanceForNewMetrics(name, showAllData, newMetrics) - ) { - return; - } - await this._recreateChoroplethLayer(name, attribution, showAllData); - } - - async _updateChoroplethLayerForNewProperties(name, attribution, showAllData) { - if (this._choroplethLayer && this._choroplethLayer.canReuseInstance(name, showAllData)) { - return; - } - await this._recreateChoroplethLayer(name, attribution, showAllData); - } - - async _recreateChoroplethLayer(name, attribution, showAllData) { - const selectedLayer = await this._loadConfig(this._params.selectedLayer); - this._kibanaMap.removeLayer(this._choroplethLayer); - - if (this._choroplethLayer) { - this._choroplethLayer = this._choroplethLayer.cloneChoroplethLayerForNewData( - name, - attribution, - selectedLayer.format, - showAllData, - selectedLayer.meta, - selectedLayer, - await getServiceSettings(), - (await lazyLoadMapsLegacyModules()).L - ); - } else { - const { ChoroplethLayer } = await import('./choropleth_layer'); - this._choroplethLayer = new ChoroplethLayer( - name, - attribution, - selectedLayer.format, - showAllData, - selectedLayer.meta, - selectedLayer, - await getServiceSettings(), - (await lazyLoadMapsLegacyModules()).L - ); - } - - this._choroplethLayer.on('select', (event) => { - const { rows, columns } = this._chartData; - const rowIndex = rows.findIndex((row) => row[columns[0].id] === event); - this.handlers.event({ - name: 'filterBucket', - data: { - data: [ - { - table: this._chartData, - column: 0, - row: rowIndex, - value: event, - }, - ], - }, - }); - }); - - this._choroplethLayer.on('styleChanged', (event) => { - const shouldShowWarning = - this._params.isDisplayWarning && uiSettings.get('visualization:regionmap:showWarnings'); - if (event.mismatches.length > 0 && shouldShowWarning) { - getNotifications().toasts.addWarning({ - title: i18n.translate('regionMap.visualization.unableToShowMismatchesWarningTitle', { - defaultMessage: - 'Unable to show {mismatchesLength} {oneMismatch, plural, one {result} other {results}} on map', - values: { - mismatchesLength: event.mismatches.length, - oneMismatch: event.mismatches.length > 1 ? 0 : 1, - }, - }), - text: i18n.translate('regionMap.visualization.unableToShowMismatchesWarningText', { - defaultMessage: - "Ensure that each of these term matches a shape on that shape's join field: {mismatches}", - values: { - mismatches: event.mismatches ? event.mismatches.join(', ') : '', - }, - }), - }); - } - }); - - this._kibanaMap.addLayer(this._choroplethLayer); - } - - _hasColumns() { - return this._chartData && this._chartData.columns.length === 2; - } - }; -} diff --git a/src/plugins/region_map/public/region_map_visualization.scss b/src/plugins/region_map/public/region_map_visualization.scss deleted file mode 100644 index ee593e2fc9c8c..0000000000000 --- a/src/plugins/region_map/public/region_map_visualization.scss +++ /dev/null @@ -1,4 +0,0 @@ -.rgmChart__wrapper, .rgmChart { - flex: 1 1 0; - display: flex; -} diff --git a/src/plugins/region_map/public/region_map_visualization_component.tsx b/src/plugins/region_map/public/region_map_visualization_component.tsx deleted file mode 100644 index cab2294475982..0000000000000 --- a/src/plugins/region_map/public/region_map_visualization_component.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useEffect, useMemo, useRef } from 'react'; -import { EuiResizeObserver } from '@elastic/eui'; -import { throttle } from 'lodash'; - -import { IInterpreterRenderHandlers, Datatable } from 'src/plugins/expressions'; -import { PersistedState } from 'src/plugins/visualizations/public'; -import { RegionMapVisualizationDependencies } from './plugin'; -import { RegionMapVisConfig } from './region_map_types'; -// @ts-expect-error -import { createRegionMapVisualization } from './region_map_visualization'; - -import './region_map_visualization.scss'; - -interface RegionMapVisController { - render(visData?: Datatable, visConfig?: RegionMapVisConfig): Promise; - resize(): void; - destroy(): void; -} - -interface TileMapVisualizationProps { - deps: RegionMapVisualizationDependencies; - handlers: IInterpreterRenderHandlers; - visData: Datatable; - visConfig: RegionMapVisConfig; -} - -const RegionMapVisualization = ({ - deps, - handlers, - visData, - visConfig, -}: TileMapVisualizationProps) => { - const chartDiv = useRef(null); - const visController = useRef(null); - const isFirstRender = useRef(true); - const uiState = handlers.uiState as PersistedState | undefined; - - useEffect(() => { - if (chartDiv.current && isFirstRender.current) { - isFirstRender.current = false; - const Controller = createRegionMapVisualization(deps); - visController.current = new Controller(chartDiv.current, handlers, visConfig); - } - }, [deps, handlers, visConfig, visData]); - - useEffect(() => { - visController.current?.render(visData, visConfig).then(handlers.done); - }, [visData, visConfig, handlers.done]); - - useEffect(() => { - const onUiStateChange = () => { - visController.current?.render().then(handlers.done); - }; - - uiState?.on('change', onUiStateChange); - - return () => { - uiState?.off('change', onUiStateChange); - }; - }, [uiState, handlers.done]); - - useEffect(() => { - return () => { - visController.current?.destroy(); - visController.current = null; - }; - }, []); - - const updateChartSize = useMemo(() => throttle(() => visController.current?.resize(), 300), []); - - return ( - - {(resizeRef) => ( -
-
-
- )} - - ); -}; - -// default export required for React.Lazy -// eslint-disable-next-line import/no-default-export -export { RegionMapVisualization as default }; diff --git a/src/plugins/region_map/public/to_ast.ts b/src/plugins/region_map/public/to_ast.ts deleted file mode 100644 index b68731cab30bb..0000000000000 --- a/src/plugins/region_map/public/to_ast.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '../../data/public'; -import { buildExpression, buildExpressionFunction } from '../../expressions/public'; -import { getVisSchemas, VisToExpressionAst } from '../../visualizations/public'; -import { RegionMapExpressionFunctionDefinition } from './region_map_fn'; -import { RegionMapVisConfig, RegionMapVisParams } from './region_map_types'; - -export const toExpressionAst: VisToExpressionAst = (vis, params) => { - const esaggs = buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: false, - partialRows: false, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); - - const schemas = getVisSchemas(vis, params); - - const visConfig: RegionMapVisConfig = { - ...vis.params, - metric: schemas.metric[0], - }; - - if (schemas.segment) { - visConfig.bucket = schemas.segment[0]; - } - - const regionmap = buildExpressionFunction('regionmap', { - visConfig: JSON.stringify(visConfig), - }); - - const ast = buildExpression([esaggs, regionmap]); - - return ast.toAst(); -}; diff --git a/src/plugins/region_map/public/tooltip_formatter.js b/src/plugins/region_map/public/tooltip_formatter.js deleted file mode 100644 index 101d49b9d88b8..0000000000000 --- a/src/plugins/region_map/public/tooltip_formatter.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function tooltipFormatter(metric, fieldFormatter, fieldName, metricName) { - if (!metric) { - return ''; - } - - const details = []; - if (fieldName && metric) { - details.push({ - label: fieldName, - value: metric.term, - }); - } - - if (metric) { - details.push({ - label: metricName, - value: fieldFormatter ? fieldFormatter.convert(metric.value, 'text') : metric.value, - }); - } - return details; -} diff --git a/src/plugins/region_map/public/util.ts b/src/plugins/region_map/public/util.ts deleted file mode 100644 index 8e15a72b0365d..0000000000000 --- a/src/plugins/region_map/public/util.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FileLayer, VectorLayer, ORIGIN } from '../../maps_ems/public'; - -export const mapToLayerWithId = (prefix: string, layer: FileLayer): VectorLayer => ({ - ...layer, - layerId: `${prefix}.${layer.name}`, - isEMS: ORIGIN.EMS === prefix, -}); diff --git a/src/plugins/region_map/server/index.ts b/src/plugins/region_map/server/index.ts deleted file mode 100644 index eb185c773458b..0000000000000 --- a/src/plugins/region_map/server/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { CoreSetup } from 'src/core/server'; -import { getUiSettings } from './ui_settings'; - -export const plugin = () => ({ - setup(core: CoreSetup) { - core.uiSettings.register(getUiSettings()); - }, - - start() {}, -}); diff --git a/src/plugins/region_map/server/ui_settings.ts b/src/plugins/region_map/server/ui_settings.ts deleted file mode 100644 index 30b48064703c2..0000000000000 --- a/src/plugins/region_map/server/ui_settings.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { UiSettingsParams } from 'kibana/server'; -import { schema } from '@kbn/config-schema'; - -export function getUiSettings(): Record> { - return { - 'visualization:regionmap:showWarnings': { - name: i18n.translate('regionMap.advancedSettings.visualization.showRegionMapWarningsTitle', { - defaultMessage: 'Show region map warning', - }), - value: true, - description: i18n.translate( - 'regionMap.advancedSettings.visualization.showRegionMapWarningsText', - { - defaultMessage: - 'Whether the region map shows a warning when terms cannot be joined to a shape on the map.', - } - ), - schema: schema.boolean(), - category: ['visualization'], - }, - }; -} diff --git a/src/plugins/region_map/tsconfig.json b/src/plugins/region_map/tsconfig.json deleted file mode 100644 index fec191402f2ab..0000000000000 --- a/src/plugins/region_map/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["public/**/*", "server/**/*"], - "references": [ - { "path": "../maps_legacy/tsconfig.json" }, - { "path": "../maps_ems/tsconfig.json" }, - { "path": "../vis_default_editor/tsconfig.json" }, - ] -} diff --git a/src/plugins/saved_objects/public/finder/index.ts b/src/plugins/saved_objects/public/finder/index.ts index edec012d90d6f..de6a54795fce5 100644 --- a/src/plugins/saved_objects/public/finder/index.ts +++ b/src/plugins/saved_objects/public/finder/index.ts @@ -9,5 +9,6 @@ export { SavedObjectMetaData, SavedObjectFinderUi, + SavedObjectFinderUiProps, getSavedObjectFinder, } from './saved_object_finder'; diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 84c39168d82c2..bc84298a63717 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -17,7 +17,12 @@ export { SaveResult, showSaveModal, } from './save_modal'; -export { getSavedObjectFinder, SavedObjectFinderUi, SavedObjectMetaData } from './finder'; +export { + getSavedObjectFinder, + SavedObjectFinderUi, + SavedObjectFinderUiProps, + SavedObjectMetaData, +} from './finder'; export { SavedObjectLoader, SavedObjectLoaderFindOptions, diff --git a/src/plugins/screenshot_mode/kibana.json b/src/plugins/screenshot_mode/kibana.json index 67c40b20be525..98942569dfac8 100644 --- a/src/plugins/screenshot_mode/kibana.json +++ b/src/plugins/screenshot_mode/kibana.json @@ -1,5 +1,9 @@ { "id": "screenshotMode", + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "version": "1.0.0", "kibanaVersion": "kibana", "ui": true, diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index 8b1d28b1606d4..5580b723a095a 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredBundles": ["kibanaUtils"], "optionalPlugins": ["securityOss"] } diff --git a/src/plugins/tile_map/README.md b/src/plugins/tile_map/README.md deleted file mode 100644 index 633ee7dba46d6..0000000000000 --- a/src/plugins/tile_map/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Coordinate map visualization - -Create a coordinate map. Display the results of a geohash_tile aggregation as bubbles, rectangles, or heatmap color blobs. - -This plugin is targeted for removal in 8.0. \ No newline at end of file diff --git a/src/plugins/tile_map/kibana.json b/src/plugins/tile_map/kibana.json deleted file mode 100644 index 16be04b5189de..0000000000000 --- a/src/plugins/tile_map/kibana.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "tileMap", - "version": "8.0.0", - "kibanaVersion": "kibana", - "ui": true, - "server": true, - "requiredPlugins": [ - "visualizations", - "expressions", - "mapsLegacy", - "mapsEms", - "kibanaLegacy", - "data", - "share" - ], - "requiredBundles": [ - "kibanaUtils", - "charts", - "visDefaultEditor" - ] -} diff --git a/src/plugins/tile_map/package.json b/src/plugins/tile_map/package.json deleted file mode 100644 index d9d0359f66048..0000000000000 --- a/src/plugins/tile_map/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "tile_map", - "version": "kibana" -} \ No newline at end of file diff --git a/src/plugins/tile_map/public/__snapshots__/tile_map_fn.test.ts.snap b/src/plugins/tile_map/public/__snapshots__/tile_map_fn.test.ts.snap deleted file mode 100644 index 7aab8b02890c0..0000000000000 --- a/src/plugins/tile_map/public/__snapshots__/tile_map_fn.test.ts.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`interpreter/functions#tilemap returns an object with the correct structure 1`] = ` -Object { - "as": "tile_map_vis", - "type": "render", - "value": Object { - "visConfig": Object { - "addTooltip": true, - "colorSchema": "Yellow to Red", - "dimensions": Object { - "geocentroid": null, - "geohash": null, - "metric": Object { - "accessor": 0, - "aggType": "count", - "format": Object { - "id": "number", - }, - "params": Object {}, - }, - }, - "heatClusterSize": 1.5, - "isDesaturated": true, - "legendPosition": "bottomright", - "mapCenter": Array [ - 0, - 0, - ], - "mapType": "Scaled Circle Markers", - "mapZoom": 2, - "wms": Object { - "enabled": false, - "options": Object { - "format": "image/png", - "transparent": true, - }, - }, - }, - "visData": Object { - "featureCollection": Object { - "features": Array [], - "type": "FeatureCollection", - }, - "meta": Object { - "geohashGridDimensionsAtEquator": null, - "geohashPrecision": null, - "max": null, - "min": null, - }, - }, - "visType": "tile_map", - }, -} -`; diff --git a/src/plugins/tile_map/public/components/collections.ts b/src/plugins/tile_map/public/components/collections.ts deleted file mode 100644 index f75d83c4a055f..0000000000000 --- a/src/plugins/tile_map/public/components/collections.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { MapTypes } from '../utils/map_types'; - -export const collections = { - mapTypes: [ - { - value: MapTypes.ScaledCircleMarkers, - text: i18n.translate('tileMap.mapTypes.scaledCircleMarkersText', { - defaultMessage: 'Scaled circle markers', - }), - }, - { - value: MapTypes.ShadedCircleMarkers, - text: i18n.translate('tileMap.mapTypes.shadedCircleMarkersText', { - defaultMessage: 'Shaded circle markers', - }), - }, - { - value: MapTypes.ShadedGeohashGrid, - text: i18n.translate('tileMap.mapTypes.shadedGeohashGridText', { - defaultMessage: 'Shaded geohash grid', - }), - }, - { - value: MapTypes.Heatmap, - text: i18n.translate('tileMap.mapTypes.heatmapText', { - defaultMessage: 'Heatmap', - }), - }, - ], - legendPositions: [ - { - value: 'bottomleft', - text: i18n.translate('tileMap.legendPositions.bottomLeftText', { - defaultMessage: 'Bottom left', - }), - }, - { - value: 'bottomright', - text: i18n.translate('tileMap.legendPositions.bottomRightText', { - defaultMessage: 'Bottom right', - }), - }, - { - value: 'topleft', - text: i18n.translate('tileMap.legendPositions.topLeftText', { - defaultMessage: 'Top left', - }), - }, - { - value: 'topright', - text: i18n.translate('tileMap.legendPositions.topRightText', { - defaultMessage: 'Top right', - }), - }, - ], -}; diff --git a/src/plugins/tile_map/public/components/index.tsx b/src/plugins/tile_map/public/components/index.tsx deleted file mode 100644 index 31c8faec2409d..0000000000000 --- a/src/plugins/tile_map/public/components/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { lazy } from 'react'; -import type { TileMapOptionsProps } from './tile_map_options'; - -const TileMapOptions = lazy(() => import('./tile_map_options')); - -export const TileMapOptionsLazy = (props: TileMapOptionsProps) => ; diff --git a/src/plugins/tile_map/public/components/tile_map_options.tsx b/src/plugins/tile_map/public/components/tile_map_options.tsx deleted file mode 100644 index dbe28f0e2c2dd..0000000000000 --- a/src/plugins/tile_map/public/components/tile_map_options.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useEffect } from 'react'; -import { EuiPanel, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { - BasicOptions, - SelectOption, - SwitchOption, - RangeOption, -} from '../../../vis_default_editor/public'; -import { truncatedColorSchemas } from '../../../charts/public'; -import { WmsOptions } from '../../../maps_legacy/public'; -import { TileMapVisParams } from '../types'; -import { MapTypes } from '../utils/map_types'; -import { getTmsLayers } from '../services'; -import { collections } from './collections'; - -export type TileMapOptionsProps = VisEditorOptionsProps; - -const tmsLayers = getTmsLayers(); - -function TileMapOptions(props: TileMapOptionsProps) { - const { stateParams, setValue, vis } = props; - - useEffect(() => { - if (!stateParams.mapType) { - setValue('mapType', collections.mapTypes[0].value); - } - }, [setValue, stateParams.mapType]); - - return ( - <> - - - - {stateParams.mapType === MapTypes.Heatmap ? ( - - ) : ( - - )} - - - - - - - - - - - ); -} - -// default export required for React.Lazy -// eslint-disable-next-line import/no-default-export -export { TileMapOptions as default }; diff --git a/src/plugins/tile_map/public/css_filters.js b/src/plugins/tile_map/public/css_filters.js deleted file mode 100644 index c1d5e68d5273f..0000000000000 --- a/src/plugins/tile_map/public/css_filters.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; - -/** - * just a place to put feature detection checks - */ -export const supportsCssFilters = (function () { - const e = document.createElement('img'); - const rules = ['webkitFilter', 'mozFilter', 'msFilter', 'filter']; - const test = 'grayscale(1)'; - - rules.forEach(function (rule) { - e.style[rule] = test; - }); - - document.body.appendChild(e); - const styles = window.getComputedStyle(e); - const can = _(styles).pick(rules).includes(test); - document.body.removeChild(e); - - return can; -})(); diff --git a/src/plugins/tile_map/public/geohash_layer.js b/src/plugins/tile_map/public/geohash_layer.js deleted file mode 100644 index e84a23c04056a..0000000000000 --- a/src/plugins/tile_map/public/geohash_layer.js +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { min, isEqual } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { KibanaMapLayer } from '../../maps_legacy/public'; -import { HeatmapMarkers } from './markers/heatmap'; -import { ScaledCirclesMarkers } from './markers/scaled_circles'; -import { ShadedCirclesMarkers } from './markers/shaded_circles'; -import { GeohashGridMarkers } from './markers/geohash_grid'; -import { MapTypes } from './utils/map_types'; - -export class GeohashLayer extends KibanaMapLayer { - constructor(featureCollection, featureCollectionMetaData, options, zoom, kibanaMap, leaflet) { - super(); - - this._featureCollection = featureCollection; - this._featureCollectionMetaData = featureCollectionMetaData; - - this._geohashOptions = options; - this._zoom = zoom; - this._kibanaMap = kibanaMap; - this._leaflet = leaflet; - const geojson = this._leaflet.geoJson(this._featureCollection); - this._bounds = geojson.getBounds(); - this._createGeohashMarkers(); - this._lastBounds = null; - } - - _createGeohashMarkers() { - const markerOptions = { - isFilteredByCollar: this._geohashOptions.isFilteredByCollar, - valueFormatter: this._geohashOptions.valueFormatter, - tooltipFormatter: this._geohashOptions.tooltipFormatter, - label: this._geohashOptions.label, - colorRamp: this._geohashOptions.colorRamp, - }; - switch (this._geohashOptions.mapType) { - case MapTypes.ScaledCircleMarkers: - this._geohashMarkers = new ScaledCirclesMarkers( - this._featureCollection, - this._featureCollectionMetaData, - markerOptions, - this._zoom, - this._kibanaMap, - this._leaflet - ); - break; - case MapTypes.ShadedCircleMarkers: - this._geohashMarkers = new ShadedCirclesMarkers( - this._featureCollection, - this._featureCollectionMetaData, - markerOptions, - this._zoom, - this._kibanaMap, - this._leaflet - ); - break; - case MapTypes.ShadedGeohashGrid: - this._geohashMarkers = new GeohashGridMarkers( - this._featureCollection, - this._featureCollectionMetaData, - markerOptions, - this._zoom, - this._kibanaMap, - this._leaflet - ); - break; - case MapTypes.Heatmap: - let radius = 15; - if (this._featureCollectionMetaData.geohashGridDimensionsAtEquator) { - const minGridLength = min(this._featureCollectionMetaData.geohashGridDimensionsAtEquator); - const metersPerPixel = this._kibanaMap.getMetersPerPixel(); - radius = minGridLength / metersPerPixel / 2; - } - radius = radius * parseFloat(this._geohashOptions.heatmap.heatClusterSize); - this._geohashMarkers = new HeatmapMarkers( - this._featureCollection, - { - radius: radius, - blur: radius, - maxZoom: this._kibanaMap.getZoomLevel(), - minOpacity: 0.1, - tooltipFormatter: this._geohashOptions.tooltipFormatter, - }, - this._zoom, - this._featureCollectionMetaData.max, - this._leaflet - ); - break; - default: - throw new Error( - i18n.translate('tileMap.geohashLayer.mapTitle', { - defaultMessage: '{mapType} mapType not recognized', - values: { - mapType: this._geohashOptions.mapType, - }, - }) - ); - } - - this._geohashMarkers.on('showTooltip', (event) => this.emit('showTooltip', event)); - this._geohashMarkers.on('hideTooltip', (event) => this.emit('hideTooltip', event)); - this._leafletLayer = this._geohashMarkers.getLeafletLayer(); - } - - appendLegendContents(jqueryDiv) { - return this._geohashMarkers.appendLegendContents(jqueryDiv); - } - - movePointer(...args) { - this._geohashMarkers.movePointer(...args); - } - - async getBounds() { - if (this._geohashOptions.fetchBounds) { - const geoHashBounds = await this._geohashOptions.fetchBounds(); - if (geoHashBounds) { - const northEast = this._leaflet.latLng( - geoHashBounds.top_left.lat, - geoHashBounds.bottom_right.lon - ); - const southWest = this._leaflet.latLng( - geoHashBounds.bottom_right.lat, - geoHashBounds.top_left.lon - ); - return this._leaflet.latLngBounds(southWest, northEast); - } - } - - return this._bounds; - } - - updateExtent() { - // Client-side filtering is only enabled when server-side filter is not used - if (!this._geohashOptions.isFilteredByCollar) { - const bounds = this._kibanaMap.getLeafletBounds(); - if (!this._lastBounds || !this._lastBounds.equals(bounds)) { - //this removal is required to trigger the bounds filter again - this._kibanaMap.removeLayer(this); - this._createGeohashMarkers(); - this._kibanaMap.addLayer(this); - } - this._lastBounds = bounds; - } - } - - isReusable(options) { - if (isEqual(this._geohashOptions, options)) { - return true; - } - - //check if any impacts leaflet styler function - if (this._geohashOptions.colorRamp !== options.colorRamp) { - return false; - } else if (this._geohashOptions.mapType !== options.mapType) { - return false; - } else if ( - this._geohashOptions.mapType === 'Heatmap' && - !isEqual(this._geohashOptions.heatmap, options) - ) { - return false; - } else { - return true; - } - } -} diff --git a/src/plugins/tile_map/public/get_deprecation_message.tsx b/src/plugins/tile_map/public/get_deprecation_message.tsx deleted file mode 100644 index 6f71aa15b8a6b..0000000000000 --- a/src/plugins/tile_map/public/get_deprecation_message.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { getQueryService, getShareService } from './services'; -import { indexPatterns } from '../../data/public'; -import { Vis } from '../../visualizations/public'; -import { LegacyMapDeprecationMessage } from '../../maps_legacy/public'; - -export function getDeprecationMessage(vis: Vis) { - const title = i18n.translate('tileMap.vis.mapTitle', { - defaultMessage: 'Coordinate Map', - }); - - async function onClick(e: React.MouseEvent) { - e.preventDefault(); - - const locator = getShareService().url.locators.get('MAPS_APP_TILE_MAP_LOCATOR'); - if (!locator) return; - - const query = getQueryService(); - const params: { [key: string]: any } = { - label: vis.title ? vis.title : title, - mapType: vis.params.mapType, - colorSchema: vis.params.colorSchema, - indexPatternId: vis.data.indexPattern?.id, - metricAgg: 'count', - filters: query.filterManager.getFilters(), - query: query.queryString.getQuery(), - timeRange: query.timefilter.timefilter.getTime(), - }; - - const bucketAggs = vis.data?.aggs?.byType('buckets'); - if (bucketAggs?.length && bucketAggs[0].type.dslName === 'geohash_grid') { - params.geoFieldName = bucketAggs[0].getField()?.name; - } else if (vis.data.indexPattern) { - // attempt to default to first geo point field when geohash is not configured yet - const geoField = vis.data.indexPattern.fields.find((field) => { - return ( - !indexPatterns.isNestedField(field) && field.aggregatable && field.type === 'geo_point' - ); - }); - if (geoField) { - params.geoFieldName = geoField.name; - } - } - - const metricAggs = vis.data?.aggs?.byType('metrics'); - if (metricAggs?.length) { - params.metricAgg = metricAggs[0].type.dslName; - params.metricFieldName = metricAggs[0].getField()?.name; - } - - locator.navigate(params); - } - - return ( - - ); -} diff --git a/src/plugins/tile_map/public/index.ts b/src/plugins/tile_map/public/index.ts deleted file mode 100644 index 3104dde0d5ac6..0000000000000 --- a/src/plugins/tile_map/public/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { TileMapPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/plugins/tile_map/public/markers/geohash_grid.js b/src/plugins/tile_map/public/markers/geohash_grid.js deleted file mode 100644 index c8c327d7a8a61..0000000000000 --- a/src/plugins/tile_map/public/markers/geohash_grid.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ScaledCirclesMarkers } from './scaled_circles'; - -export class GeohashGridMarkers extends ScaledCirclesMarkers { - getMarkerFunction() { - return (feature) => { - const geohashRect = feature.properties.geohash_meta.rectangle; - // get bounds from northEast[3] and southWest[1] - // corners in geohash rectangle - const corners = [ - [geohashRect[3][0], geohashRect[3][1]], - [geohashRect[1][0], geohashRect[1][1]], - ]; - return this._leaflet.rectangle(corners); - }; - } -} diff --git a/src/plugins/tile_map/public/markers/heatmap.js b/src/plugins/tile_map/public/markers/heatmap.js deleted file mode 100644 index 5b5649b1e992c..0000000000000 --- a/src/plugins/tile_map/public/markers/heatmap.js +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import d3 from 'd3'; -import { EventEmitter } from 'events'; - -/** - * Map overlay: canvas layer with leaflet.heat plugin - * - * @param map {Leaflet Object} - * @param geoJson {geoJson Object} - * @param params {Object} - */ -export class HeatmapMarkers extends EventEmitter { - constructor(featureCollection, options, zoom, max, leaflet) { - super(); - this._geojsonFeatureCollection = featureCollection; - const points = dataToHeatArray(featureCollection, max); - this._leafletLayer = new leaflet.HeatLayer(points, options); - this._tooltipFormatter = options.tooltipFormatter; - this._zoom = zoom; - this._disableTooltips = false; - this._getLatLng = _.memoize( - function (feature) { - return leaflet.latLng(feature.geometry.coordinates[1], feature.geometry.coordinates[0]); - }, - function (feature) { - // turn coords into a string for the memoize cache - return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(','); - } - ); - this._addTooltips(); - } - - getBounds() { - return this._leafletLayer.getBounds(); - } - - getLeafletLayer() { - return this._leafletLayer; - } - - appendLegendContents() {} - - movePointer(type, event) { - if (type === 'mousemove') { - this._debounceMoveMoveLocation(event); - } else if (type === 'mouseout') { - this.emit('hideTooltip'); - } else if (type === 'mousedown') { - this._disableTooltips = true; - this.emit('hideTooltip'); - } else if (type === 'mouseup') { - this._disableTooltips = false; - } - } - - _addTooltips() { - const mouseMoveLocation = (e) => { - if (!this._geojsonFeatureCollection.features.length || this._disableTooltips) { - this.emit('hideTooltip'); - return; - } - - const feature = this._nearestFeature(e.latlng); - if (this._tooltipProximity(e.latlng, feature)) { - const content = this._tooltipFormatter(feature); - if (!content) { - return; - } - this.emit('showTooltip', { - content: content, - position: e.latlng, - }); - } else { - this.emit('hideTooltip'); - } - }; - - this._debounceMoveMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, { - leading: true, - trailing: false, - }); - } - - /** - * Finds nearest feature in mapData to event latlng - * - * @method _nearestFeature - * @param latLng {Leaflet latLng} - * @return nearestPoint {Leaflet latLng} - */ - _nearestFeature(latLng) { - const self = this; - let nearest; - - if (latLng.lng < -180 || latLng.lng > 180) { - return; - } - - _.reduce( - this._geojsonFeatureCollection.features, - function (distance, feature) { - const featureLatLng = self._getLatLng(feature); - const dist = latLng.distanceTo(featureLatLng); - - if (dist < distance) { - nearest = feature; - return dist; - } - - return distance; - }, - Infinity - ); - - return nearest; - } - - /** - * display tooltip if feature is close enough to event latlng - * - * @method _tooltipProximity - * @param latlng {Leaflet latLng Object} - * @param feature {geoJson Object} - * @return {Boolean} - */ - _tooltipProximity(latlng, feature) { - if (!feature) return; - - let showTip = false; - const featureLatLng = this._getLatLng(feature); - - // zoomScale takes map zoom and returns proximity value for tooltip display - // domain (input values) is map zoom (min 1 and max 18) - // range (output values) is distance in meters - // used to compare proximity of event latlng to feature latlng - const zoomScale = d3.scale - .linear() - .domain([1, 4, 7, 10, 13, 16, 18]) - .range([1000000, 300000, 100000, 15000, 2000, 150, 50]); - - const proximity = zoomScale(this._zoom); - const distance = latlng.distanceTo(featureLatLng); - - // maxLngDif is max difference in longitudes - // to prevent feature tooltip from appearing 360° - // away from event latlng - const maxLngDif = 40; - const lngDif = Math.abs(latlng.lng - featureLatLng.lng); - - if (distance < proximity && lngDif < maxLngDif) { - showTip = true; - } - - d3.scale.pow().exponent(0.2).domain([1, 18]).range([1500000, 50]); - return showTip; - } -} - -/** - * returns normalized data for heat map intensity - * - * @method dataToHeatArray - * @param featureCollection {Array} - * @return {Array} - */ -function dataToHeatArray(featureCollection, max) { - return featureCollection.features.map((feature) => { - const lat = feature.geometry.coordinates[1]; - const lng = feature.geometry.coordinates[0]; - // show bucket value normalized to max value - const heatIntensity = feature.properties.value / max; - - return [lat, lng, heatIntensity]; - }); -} diff --git a/src/plugins/tile_map/public/markers/scaled_circles.js b/src/plugins/tile_map/public/markers/scaled_circles.js deleted file mode 100644 index 54fba95953255..0000000000000 --- a/src/plugins/tile_map/public/markers/scaled_circles.js +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import d3 from 'd3'; -import $ from 'jquery'; -import { EventEmitter } from 'events'; -import { colorUtil } from '../../../maps_legacy/public'; -import { truncatedColorMaps } from '../../../charts/public'; - -export class ScaledCirclesMarkers extends EventEmitter { - constructor( - featureCollection, - featureCollectionMetaData, - options, - targetZoom, - kibanaMap, - leaflet - ) { - super(); - this._featureCollection = featureCollection; - this._featureCollectionMetaData = featureCollectionMetaData; - - this._zoom = targetZoom; - - this._valueFormatter = - options.valueFormatter || - ((x) => { - x; - }); - this._tooltipFormatter = - options.tooltipFormatter || - ((x) => { - x; - }); - this._label = options.label; - this._colorRamp = options.colorRamp; - - this._legendColors = null; - this._legendQuantizer = null; - this._leaflet = leaflet; - - this._popups = []; - - const layerOptions = { - pointToLayer: this.getMarkerFunction(), - style: this.getStyleFunction(), - onEachFeature: (feature, layer) => { - this._bindPopup(feature, layer); - }, - }; - // Filter leafletlayer on client when results are not filtered on the server - if (!options.isFilteredByCollar) { - layerOptions.filter = (feature) => { - const bucketRectBounds = feature.properties.geohash_meta.rectangle; - return kibanaMap.isInside(bucketRectBounds); - }; - } - this._leafletLayer = this._leaflet.geoJson(null, layerOptions); - this._leafletLayer.addData(this._featureCollection); - } - - getLeafletLayer() { - return this._leafletLayer; - } - - getStyleFunction() { - const min = _.get(this._featureCollectionMetaData, 'min', 0); - const max = _.get(this._featureCollectionMetaData, 'max', 1); - - const quantizeDomain = min !== max ? [min, max] : d3.scale.quantize().domain(); - - this._legendColors = this.getLegendColors(); - this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors); - - return makeStyleFunction(this._legendColors, quantizeDomain); - } - - movePointer() {} - - getLabel() { - if (this._popups.length) { - return this._label; - } - return ''; - } - - appendLegendContents(jqueryDiv) { - if (!this._legendColors || !this._legendQuantizer) { - return; - } - - const titleText = this.getLabel(); - const $title = $('
').addClass('visMapLegend__title').text(titleText); - jqueryDiv.append($title); - - this._legendColors.forEach((color) => { - const labelText = this._legendQuantizer - .invertExtent(color) - .map(this._valueFormatter) - .join(' – '); - - const label = $('
'); - const icon = $('').css({ - background: color, - 'border-color': makeColorDarker(color), - }); - - const text = $('').text(labelText); - label.append(icon); - label.append(text); - - jqueryDiv.append(label); - }); - } - - /** - * Binds popup and events to each feature on map - * - * @method bindPopup - * @param feature {Object} - * @param layer {Object} - * return {undefined} - */ - _bindPopup(feature, layer) { - const popup = layer.on({ - mouseover: (e) => { - const layer = e.target; - // bring layer to front if not older browser - if (!this._leaflet.Browser.ie && !this._leaflet.Browser.opera) { - layer.bringToFront(); - } - this._showTooltip(feature); - }, - mouseout: () => { - this.emit('hideTooltip'); - }, - }); - - this._popups.push(popup); - } - - /** - * Checks if event latlng is within bounds of mapData - * features and shows tooltip for that feature - * - * @method _showTooltip - * @param feature {LeafletFeature} - * @return undefined - */ - _showTooltip(feature) { - const content = this._tooltipFormatter(feature); - if (!content) { - return; - } - - const latLng = this._leaflet.latLng( - feature.geometry.coordinates[1], - feature.geometry.coordinates[0] - ); - this.emit('showTooltip', { - content: content, - position: latLng, - }); - } - - getMarkerFunction() { - const scaleFactor = 0.6; - return (feature, latlng) => { - const value = feature.properties.value; - const scaledRadius = this._radiusScale(value) * scaleFactor; - return this._leaflet.circleMarker(latlng).setRadius(scaledRadius); - }; - } - - /** - * radiusScale returns a number for scaled circle markers - * for relative sizing of markers - * - * @method _radiusScale - * @param value {Number} - * @return {Number} - */ - _radiusScale(value) { - //magic numbers - const precisionBiasBase = 5; - const precisionBiasNumerator = 200; - - const precision = _.max( - this._featureCollection.features.map((feature) => { - return String(feature.properties.geohash).length; - }) - ); - - const pct = Math.abs(value) / Math.abs(this._featureCollectionMetaData.max); - const zoomRadius = 0.5 * Math.pow(2, this._zoom); - const precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision); - - // square root value percentage - return Math.pow(pct, 0.5) * zoomRadius * precisionScale; - } - - getBounds() { - return this._leafletLayer.getBounds(); - } - - getLegendColors() { - const colorRamp = _.get(truncatedColorMaps[this._colorRamp], 'value'); - return colorUtil.getLegendColors(colorRamp); - } -} - -function makeColorDarker(color) { - const amount = 1.3; //magic number, carry over from earlier - return d3.hcl(color).darker(amount).toString(); -} - -function makeStyleFunction(legendColors, quantizeDomain) { - const legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(legendColors); - return (feature) => { - const value = _.get(feature, 'properties.value'); - const color = legendQuantizer(value); - return { - fillColor: color, - color: makeColorDarker(color), - weight: 1.5, - opacity: 1, - fillOpacity: 0.75, - }; - }; -} diff --git a/src/plugins/tile_map/public/markers/shaded_circles.js b/src/plugins/tile_map/public/markers/shaded_circles.js deleted file mode 100644 index a47fcd71427f3..0000000000000 --- a/src/plugins/tile_map/public/markers/shaded_circles.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import { ScaledCirclesMarkers } from './scaled_circles'; - -export class ShadedCirclesMarkers extends ScaledCirclesMarkers { - getMarkerFunction() { - // multiplier to reduce size of all circles - const scaleFactor = 0.8; - return (feature, latlng) => { - const radius = this._geohashMinDistance(feature) * scaleFactor; - return this._leaflet.circle(latlng, radius); - }; - } - - /** - * _geohashMinDistance returns a min distance in meters for sizing - * circle markers to fit within geohash grid rectangle - * - * @method _geohashMinDistance - * @param feature {Object} - * @return {Number} - */ - _geohashMinDistance(feature) { - const centerPoint = feature.properties.geohash_meta.center; - const geohashRect = feature.properties.geohash_meta.rectangle; - - // centerPoint is an array of [lat, lng] - // geohashRect is the 4 corners of the geoHash rectangle - // an array that starts at the southwest corner and proceeds - // clockwise, each value being an array of [lat, lng] - - // center lat and southeast lng - const east = this._leaflet.latLng([centerPoint[0], geohashRect[2][1]]); - // southwest lat and center lng - const north = this._leaflet.latLng([geohashRect[3][0], centerPoint[1]]); - - // get latLng of geohash center point - const center = this._leaflet.latLng([centerPoint[0], centerPoint[1]]); - - // get smallest radius at center of geohash grid rectangle - const eastRadius = Math.floor(center.distanceTo(east)); - const northRadius = Math.floor(center.distanceTo(north)); - return _.min([eastRadius, northRadius]); - } -} diff --git a/src/plugins/tile_map/public/plugin.ts b/src/plugins/tile_map/public/plugin.ts deleted file mode 100644 index 78cd12ffbccad..0000000000000 --- a/src/plugins/tile_map/public/plugin.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, - IUiSettingsClient, -} from 'kibana/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { VisualizationsSetup } from '../../visualizations/public'; -import { MapsLegacyPluginSetup } from '../../maps_legacy/public'; -import { MapsEmsPluginSetup } from '../../maps_ems/public'; -import { IServiceSettings } from '../../maps_ems/public'; -import { DataPublicPluginStart } from '../../data/public'; -import { - setCoreService, - setFormatService, - setQueryService, - setKibanaLegacy, - setShareService, -} from './services'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; -import { SharePluginStart } from '../../share/public'; - -import { createTileMapFn } from './tile_map_fn'; -import { createTileMapTypeDefinition } from './tile_map_type'; -import { getTileMapRenderer } from './tile_map_renderer'; - -/** @private */ -export interface TileMapVisualizationDependencies { - uiSettings: IUiSettingsClient; - getZoomPrecision: any; - getPrecision: any; - BaseMapsVisualization: any; - getServiceSettings: () => Promise; -} - -/** @internal */ -export interface TileMapPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - mapsLegacy: MapsLegacyPluginSetup; - mapsEms: MapsEmsPluginSetup; -} - -/** @internal */ -export interface TileMapPluginStartDependencies { - data: DataPublicPluginStart; - kibanaLegacy: KibanaLegacyStart; - share: SharePluginStart; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TileMapPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TileMapPluginStart {} - -/** @internal */ -export class TileMapPlugin implements Plugin { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public setup( - core: CoreSetup, - { expressions, visualizations, mapsLegacy, mapsEms }: TileMapPluginSetupDependencies - ) { - const { getZoomPrecision, getPrecision } = mapsLegacy; - const visualizationDependencies: Readonly = { - getZoomPrecision, - getPrecision, - BaseMapsVisualization: mapsLegacy.getBaseMapsVis(), - uiSettings: core.uiSettings, - getServiceSettings: mapsEms.getServiceSettings, - }; - - expressions.registerFunction(createTileMapFn); - expressions.registerRenderer(getTileMapRenderer(visualizationDependencies)); - - visualizations.createBaseVisualization(createTileMapTypeDefinition(visualizationDependencies)); - - return {}; - } - - public start(core: CoreStart, plugins: TileMapPluginStartDependencies) { - setFormatService(plugins.data.fieldFormats); - setQueryService(plugins.data.query); - setKibanaLegacy(plugins.kibanaLegacy); - setShareService(plugins.share); - setCoreService(core); - return {}; - } -} diff --git a/src/plugins/tile_map/public/services.ts b/src/plugins/tile_map/public/services.ts deleted file mode 100644 index cef4fbae03f38..0000000000000 --- a/src/plugins/tile_map/public/services.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { CoreStart } from 'kibana/public'; -import { createGetterSetter } from '../../kibana_utils/public'; -import { DataPublicPluginStart } from '../../data/public'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; -import { SharePluginStart } from '../../share/public'; -import { TmsLayer } from '../../maps_ems/public'; - -export const [getCoreService, setCoreService] = createGetterSetter('Core'); - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('vislib data.fieldFormats'); - -export const [getQueryService, setQueryService] = createGetterSetter< - DataPublicPluginStart['query'] ->('Query'); - -export const [getShareService, setShareService] = createGetterSetter('Share'); - -export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter( - 'KibanaLegacy' -); - -export const [getTmsLayers, setTmsLayers] = createGetterSetter('TmsLayers'); diff --git a/src/plugins/tile_map/public/tile_map_fn.test.ts b/src/plugins/tile_map/public/tile_map_fn.test.ts deleted file mode 100644 index 7e8e065e959dd..0000000000000 --- a/src/plugins/tile_map/public/tile_map_fn.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; -import { createTileMapFn } from './tile_map_fn'; - -jest.mock('./utils', () => ({ - convertToGeoJson: jest.fn().mockReturnValue({ - featureCollection: { - type: 'FeatureCollection', - features: [], - }, - meta: { - min: null, - max: null, - geohashPrecision: null, - geohashGridDimensionsAtEquator: null, - }, - }), -})); - -import { convertToGeoJson } from './utils'; - -describe('interpreter/functions#tilemap', () => { - const fn = functionWrapper(createTileMapFn()); - const context = { - type: 'datatable', - rows: [{ 'col-0-1': 0 }], - columns: [{ id: 'col-0-1', name: 'Count' }], - }; - const visConfig = { - colorSchema: 'Yellow to Red', - mapType: 'Scaled Circle Markers', - isDesaturated: true, - addTooltip: true, - heatClusterSize: 1.5, - legendPosition: 'bottomright', - mapZoom: 2, - mapCenter: [0, 0], - wms: { - enabled: false, - options: { - format: 'image/png', - transparent: true, - }, - }, - dimensions: { - metric: { - accessor: 0, - format: { - id: 'number', - }, - params: {}, - aggType: 'count', - }, - geohash: null, - geocentroid: null, - }, - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('returns an object with the correct structure', async () => { - const actual = await fn(context, { visConfig: JSON.stringify(visConfig) }); - expect(actual).toMatchSnapshot(); - }); - - it('calls response handler with correct values', async () => { - const { geohash, metric, geocentroid } = visConfig.dimensions; - await fn(context, { visConfig: JSON.stringify(visConfig) }); - expect(convertToGeoJson).toHaveBeenCalledTimes(1); - expect(convertToGeoJson).toHaveBeenCalledWith(context, { - geohash, - metric, - geocentroid, - }); - }); -}); diff --git a/src/plugins/tile_map/public/tile_map_fn.ts b/src/plugins/tile_map/public/tile_map_fn.ts deleted file mode 100644 index 53372b46c0bf8..0000000000000 --- a/src/plugins/tile_map/public/tile_map_fn.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -import type { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; -import { TileMapVisConfig, TileMapVisData } from './types'; - -interface Arguments { - visConfig: string | null; -} - -export interface TileMapVisRenderValue { - visData: TileMapVisData; - visType: 'tile_map'; - visConfig: TileMapVisConfig; -} - -export type TileMapExpressionFunctionDefinition = ExpressionFunctionDefinition< - 'tilemap', - Datatable, - Arguments, - Promise> ->; - -export const createTileMapFn = (): TileMapExpressionFunctionDefinition => ({ - name: 'tilemap', - type: 'render', - context: { - types: ['datatable'], - }, - help: i18n.translate('tileMap.function.help', { - defaultMessage: 'Tilemap visualization', - }), - args: { - visConfig: { - types: ['string', 'null'], - default: '"{}"', - help: '', - }, - }, - async fn(context, args, handlers) { - const visConfig = args.visConfig && JSON.parse(args.visConfig); - const { geohash, metric, geocentroid } = visConfig.dimensions; - - const { convertToGeoJson } = await import('./utils'); - const convertedData = convertToGeoJson(context, { - geohash, - metric, - geocentroid, - }); - - if (handlers?.inspectorAdapters?.tables) { - handlers.inspectorAdapters.tables.logDatatable('default', context); - } - return { - type: 'render', - as: 'tile_map_vis', - value: { - visData: convertedData, - visType: 'tile_map', - visConfig, - }, - }; - }, -}); diff --git a/src/plugins/tile_map/public/tile_map_renderer.tsx b/src/plugins/tile_map/public/tile_map_renderer.tsx deleted file mode 100644 index 7339e4bf64b1c..0000000000000 --- a/src/plugins/tile_map/public/tile_map_renderer.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { lazy } from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { ExpressionRenderDefinition } from 'src/plugins/expressions'; -import { VisualizationContainer } from '../../visualizations/public'; -import { TileMapVisualizationDependencies } from './plugin'; -import { TileMapVisRenderValue } from './tile_map_fn'; - -const TileMapVisualization = lazy(() => import('./tile_map_visualization_component')); - -export const getTileMapRenderer: ( - deps: TileMapVisualizationDependencies -) => ExpressionRenderDefinition = (deps) => ({ - name: 'tile_map_vis', - reuseDomNode: true, - render: async (domNode, { visConfig, visData }, handlers) => { - handlers.onDestroy(() => { - unmountComponentAtNode(domNode); - }); - - render( - - - , - domNode - ); - }, -}); diff --git a/src/plugins/tile_map/public/tile_map_type.ts b/src/plugins/tile_map/public/tile_map_type.ts deleted file mode 100644 index 5e71351f1bd56..0000000000000 --- a/src/plugins/tile_map/public/tile_map_type.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { VisTypeDefinition } from 'src/plugins/visualizations/public'; - -// @ts-expect-error -import { supportsCssFilters } from './css_filters'; -import { TileMapOptionsLazy } from './components'; -import { getDeprecationMessage } from './get_deprecation_message'; -import { TileMapVisualizationDependencies } from './plugin'; -import { toExpressionAst } from './to_ast'; -import { TileMapVisParams } from './types'; -import { setTmsLayers } from './services'; - -export function createTileMapTypeDefinition( - dependencies: TileMapVisualizationDependencies -): VisTypeDefinition { - const { uiSettings, getServiceSettings } = dependencies; - - return { - name: 'tile_map', - getInfoMessage: getDeprecationMessage, - title: i18n.translate('tileMap.vis.mapTitle', { - defaultMessage: 'Coordinate Map', - }), - icon: 'visMapCoordinate', - description: i18n.translate('tileMap.vis.mapDescription', { - defaultMessage: 'Plot latitude and longitude coordinates on a map', - }), - visConfig: { - canDesaturate: Boolean(supportsCssFilters), - defaults: { - colorSchema: 'Yellow to Red', - mapType: 'Scaled Circle Markers', - isDesaturated: true, - addTooltip: true, - heatClusterSize: 1.5, - legendPosition: 'bottomright', - mapZoom: 2, - mapCenter: [0, 0], - wms: uiSettings.get('visualization:tileMap:WMSdefaults'), - }, - }, - toExpressionAst, - editorConfig: { - optionsTemplate: TileMapOptionsLazy, - schemas: [ - { - group: 'metrics', - name: 'metric', - title: i18n.translate('tileMap.vis.map.editorConfig.schemas.metricTitle', { - defaultMessage: 'Value', - }), - min: 1, - max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'], - defaults: [{ schema: 'metric', type: 'count' }], - }, - { - group: 'buckets', - name: 'segment', - title: i18n.translate('tileMap.vis.map.editorConfig.schemas.geoCoordinatesTitle', { - defaultMessage: 'Geo coordinates', - }), - aggFilter: ['geohash_grid'], - min: 1, - max: 1, - }, - ], - }, - setup: async (vis) => { - let tmsLayers; - - try { - const serviceSettings = await getServiceSettings(); - tmsLayers = await serviceSettings.getTMSServices(); - } catch (e) { - return vis; - } - - setTmsLayers(tmsLayers); - if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) { - vis.params.wms.selectedTmsLayer = tmsLayers[0]; - } - return vis; - }, - requiresSearch: true, - }; -} diff --git a/src/plugins/tile_map/public/tile_map_visualization.js b/src/plugins/tile_map/public/tile_map_visualization.js deleted file mode 100644 index ebce2de51bbbc..0000000000000 --- a/src/plugins/tile_map/public/tile_map_visualization.js +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { get, round } from 'lodash'; -import { getFormatService, getQueryService, getKibanaLegacy } from './services'; -import { mapTooltipProvider, lazyLoadMapsLegacyModules } from '../../maps_legacy/public'; -import { tooltipFormatter } from './tooltip_formatter'; -import { geoContains } from './utils'; - -function scaleBounds(bounds) { - const scale = 0.5; // scale bounds by 50% - - const topLeft = bounds.top_left; - const bottomRight = bounds.bottom_right; - let latDiff = round(Math.abs(topLeft.lat - bottomRight.lat), 5); - const lonDiff = round(Math.abs(bottomRight.lon - topLeft.lon), 5); - // map height can be zero when vis is first created - if (latDiff === 0) latDiff = lonDiff; - - const latDelta = latDiff * scale; - let topLeftLat = round(topLeft.lat, 5) + latDelta; - if (topLeftLat > 90) topLeftLat = 90; - let bottomRightLat = round(bottomRight.lat, 5) - latDelta; - if (bottomRightLat < -90) bottomRightLat = -90; - const lonDelta = lonDiff * scale; - let topLeftLon = round(topLeft.lon, 5) - lonDelta; - if (topLeftLon < -180) topLeftLon = -180; - let bottomRightLon = round(bottomRight.lon, 5) + lonDelta; - if (bottomRightLon > 180) bottomRightLon = 180; - - return { - top_left: { lat: topLeftLat, lon: topLeftLon }, - bottom_right: { lat: bottomRightLat, lon: bottomRightLon }, - }; -} - -export const createTileMapVisualization = (dependencies) => { - const { getZoomPrecision, getPrecision, BaseMapsVisualization } = dependencies; - - return class CoordinateMapsVisualization extends BaseMapsVisualization { - constructor(element, handlers, initialVisParams) { - super(element, handlers, initialVisParams); - - this._geohashLayer = null; - this._tooltipFormatter = mapTooltipProvider(element, tooltipFormatter); - } - - updateGeohashAgg = () => { - const geohashAgg = this._getGeoHashAgg(); - if (!geohashAgg) return; - const updateVarsObject = { - name: 'bounds', - data: {}, - }; - const bounds = this._kibanaMap.getBounds(); - const mapCollar = scaleBounds(bounds); - if (!geoContains(geohashAgg.sourceParams.params.boundingBox, mapCollar)) { - updateVarsObject.data.boundingBox = { - top_left: mapCollar.top_left, - bottom_right: mapCollar.bottom_right, - }; - } else { - updateVarsObject.data.boundingBox = geohashAgg.sourceParams.params.boundingBox; - } - // todo: autoPrecision should be vis parameter, not aggConfig one - const zoomPrecision = getZoomPrecision(); - updateVarsObject.data.precision = geohashAgg.sourceParams.params.autoPrecision - ? zoomPrecision[this.handlers.uiState.get('mapZoom')] - : getPrecision(geohashAgg.sourceParams.params.precision); - - this.handlers.event(updateVarsObject); - }; - - async render(esResponse, visParams) { - getKibanaLegacy().loadFontAwesome(); - await super.render(esResponse, visParams); - } - - async _makeKibanaMap() { - await super._makeKibanaMap(this._params); - - let previousPrecision = this._kibanaMap.getGeohashPrecision(); - let precisionChange = false; - - this.handlers.uiState.on('change', (prop) => { - if (prop === 'mapZoom' || prop === 'mapCenter') { - this.updateGeohashAgg(); - } - }); - - this._kibanaMap.on('zoomchange', () => { - precisionChange = previousPrecision !== this._kibanaMap.getGeohashPrecision(); - previousPrecision = this._kibanaMap.getGeohashPrecision(); - }); - this._kibanaMap.on('zoomend', () => { - const geohashAgg = this._getGeoHashAgg(); - if (!geohashAgg) { - return; - } - const isAutoPrecision = - typeof geohashAgg.sourceParams.params.autoPrecision === 'boolean' - ? geohashAgg.sourceParams.params.autoPrecision - : true; - if (!isAutoPrecision) { - return; - } - if (precisionChange) { - this.updateGeohashAgg(); - } else { - //when we filter queries by collar - this._updateData(this._geoJsonFeatureCollectionAndMeta); - } - }); - - this._kibanaMap.addDrawControl(); - this._kibanaMap.on('drawCreated:rectangle', (event) => { - const geohashAgg = this._getGeoHashAgg(); - this.addSpatialFilter(geohashAgg, 'geo_bounding_box', event.bounds); - }); - this._kibanaMap.on('drawCreated:polygon', (event) => { - const geohashAgg = this._getGeoHashAgg(); - this.addSpatialFilter(geohashAgg, 'geo_polygon', { points: event.points }); - }); - } - - async _updateData(geojsonFeatureCollectionAndMeta) { - // Only recreate geohash layer when there is new aggregation data - // Exception is Heatmap: which needs to be redrawn every zoom level because the clustering is based on meters per pixel - if ( - this._getMapsParams().mapType !== 'Heatmap' && - geojsonFeatureCollectionAndMeta === this._geoJsonFeatureCollectionAndMeta - ) { - return; - } - - if (this._geohashLayer) { - this._kibanaMap.removeLayer(this._geohashLayer); - this._geohashLayer = null; - } - - if (!geojsonFeatureCollectionAndMeta) { - this._geoJsonFeatureCollectionAndMeta = null; - this._kibanaMap.removeLayer(this._geohashLayer); - this._geohashLayer = null; - return; - } - - if ( - !this._geoJsonFeatureCollectionAndMeta || - !geojsonFeatureCollectionAndMeta.featureCollection.features.length - ) { - this._geoJsonFeatureCollectionAndMeta = geojsonFeatureCollectionAndMeta; - this.updateGeohashAgg(); - } - - this._geoJsonFeatureCollectionAndMeta = geojsonFeatureCollectionAndMeta; - this._recreateGeohashLayer(); - } - - async _recreateGeohashLayer() { - const { GeohashLayer } = await import('./geohash_layer'); - - if (this._geohashLayer) { - this._kibanaMap.removeLayer(this._geohashLayer); - this._geohashLayer = null; - } - const geohashOptions = this._getGeohashOptions(); - this._geohashLayer = new GeohashLayer( - this._geoJsonFeatureCollectionAndMeta.featureCollection, - this._geoJsonFeatureCollectionAndMeta.meta, - geohashOptions, - this._kibanaMap.getZoomLevel(), - this._kibanaMap, - (await lazyLoadMapsLegacyModules()).L - ); - this._kibanaMap.addLayer(this._geohashLayer); - } - - async _updateParams() { - await super._updateParams(); - - this._kibanaMap.setDesaturateBaseLayer(this._params.isDesaturated); - - //avoid recreating the leaflet layer when there are option-changes that do not effect the representation - //e.g. tooltip-visibility, legend position, basemap-desaturation, ... - const geohashOptions = this._getGeohashOptions(); - if (!this._geohashLayer || !this._geohashLayer.isReusable(geohashOptions)) { - if (this._geoJsonFeatureCollectionAndMeta) { - this._recreateGeohashLayer(); - } - this._updateData(this._geoJsonFeatureCollectionAndMeta); - } - } - - _getGeohashOptions() { - const newParams = this._getMapsParams(); - const metricDimension = this._params.dimensions.metric; - const metricLabel = metricDimension ? metricDimension.label : ''; - const metricFormat = getFormatService().deserialize( - metricDimension && metricDimension.format - ); - - return { - label: metricLabel, - valueFormatter: this._geoJsonFeatureCollectionAndMeta - ? metricFormat.getConverterFor('text') - : null, - tooltipFormatter: this._geoJsonFeatureCollectionAndMeta - ? this._tooltipFormatter.bind(null, metricLabel, metricFormat.getConverterFor('text')) - : null, - mapType: newParams.mapType, - isFilteredByCollar: this._isFilteredByCollar(), - colorRamp: newParams.colorSchema, - heatmap: { - heatClusterSize: newParams.heatClusterSize, - }, - }; - } - - addSpatialFilter(agg, filterName, filterData) { - if (!agg) { - return; - } - - const indexPatternName = agg.indexPatternId; - const field = agg.field; - const filter = { meta: { negate: false, index: indexPatternName } }; - filter[filterName] = { ignore_unmapped: true }; - filter[filterName][field] = filterData; - - const { filterManager } = getQueryService(); - filterManager.addFilters([filter]); - } - - _getGeoHashAgg() { - return ( - this._geoJsonFeatureCollectionAndMeta && this._geoJsonFeatureCollectionAndMeta.meta.geohash - ); - } - - _isFilteredByCollar() { - const DEFAULT = false; - const agg = this._getGeoHashAgg(); - if (agg) { - return get(agg, 'sourceParams.params.isFilteredByCollar', DEFAULT); - } else { - return DEFAULT; - } - } - }; -}; diff --git a/src/plugins/tile_map/public/tile_map_visualization.scss b/src/plugins/tile_map/public/tile_map_visualization.scss deleted file mode 100644 index 4298b06c763da..0000000000000 --- a/src/plugins/tile_map/public/tile_map_visualization.scss +++ /dev/null @@ -1,4 +0,0 @@ -.tlmChart__wrapper, .tlmChart { - flex: 1 1 0; - display: flex; -} diff --git a/src/plugins/tile_map/public/tile_map_visualization_component.tsx b/src/plugins/tile_map/public/tile_map_visualization_component.tsx deleted file mode 100644 index 094efc63312e5..0000000000000 --- a/src/plugins/tile_map/public/tile_map_visualization_component.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useEffect, useMemo, useRef } from 'react'; -import { EuiResizeObserver } from '@elastic/eui'; -import { throttle } from 'lodash'; - -import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; -import { PersistedState } from 'src/plugins/visualizations/public'; -import { TileMapVisualizationDependencies } from './plugin'; -import { TileMapVisConfig, TileMapVisData } from './types'; -// @ts-expect-error -import { createTileMapVisualization } from './tile_map_visualization'; - -import './tile_map_visualization.scss'; - -interface TileMapVisController { - render(visData?: TileMapVisData, visConfig?: TileMapVisConfig): Promise; - resize(): void; - destroy(): void; -} - -interface TileMapVisualizationProps { - deps: TileMapVisualizationDependencies; - handlers: IInterpreterRenderHandlers; - visData: TileMapVisData; - visConfig: TileMapVisConfig; -} - -const TileMapVisualization = ({ - deps, - handlers, - visData, - visConfig, -}: TileMapVisualizationProps) => { - const chartDiv = useRef(null); - const visController = useRef(null); - const isFirstRender = useRef(true); - const uiState = handlers.uiState as PersistedState; - - useEffect(() => { - if (chartDiv.current && isFirstRender.current) { - isFirstRender.current = false; - const Controller = createTileMapVisualization(deps); - visController.current = new Controller(chartDiv.current, handlers, visConfig); - } - }, [deps, handlers, visConfig, visData]); - - useEffect(() => { - visController.current?.render(visData, visConfig).then(handlers.done); - }, [visData, visConfig, handlers.done]); - - useEffect(() => { - const onUiStateChange = () => { - visController.current?.render().then(handlers.done); - }; - - uiState.on('change', onUiStateChange); - - return () => { - uiState.off('change', onUiStateChange); - }; - }, [uiState, handlers.done]); - - useEffect(() => { - return () => { - visController.current?.destroy(); - visController.current = null; - }; - }, []); - - const updateChartSize = useMemo(() => throttle(() => visController.current?.resize(), 300), []); - - return ( - - {(resizeRef) => ( -
-
-
- )} - - ); -}; - -// default export required for React.Lazy -// eslint-disable-next-line import/no-default-export -export { TileMapVisualization as default }; diff --git a/src/plugins/tile_map/public/to_ast.ts b/src/plugins/tile_map/public/to_ast.ts deleted file mode 100644 index b23a10909661b..0000000000000 --- a/src/plugins/tile_map/public/to_ast.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - EsaggsExpressionFunctionDefinition, - IndexPatternLoadExpressionFunctionDefinition, -} from '../../data/public'; -import { buildExpression, buildExpressionFunction } from '../../expressions/public'; -import { getVisSchemas, VisToExpressionAst } from '../../visualizations/public'; -import { TileMapExpressionFunctionDefinition } from './tile_map_fn'; -import { TileMapVisConfig, TileMapVisParams } from './types'; - -export const toExpressionAst: VisToExpressionAst = (vis, params) => { - const esaggs = buildExpressionFunction('esaggs', { - index: buildExpression([ - buildExpressionFunction('indexPatternLoad', { - id: vis.data.indexPattern!.id!, - }), - ]), - metricsAtAllLevels: false, - partialRows: false, - aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), - }); - - const schemas = getVisSchemas(vis, params); - - const visConfig: TileMapVisConfig = { - ...vis.params, - dimensions: { - metric: schemas.metric[0], - geohash: schemas.segment ? schemas.segment[0] : null, - geocentroid: schemas.geo_centroid ? schemas.geo_centroid[0] : null, - }, - }; - - const tilemap = buildExpressionFunction('tilemap', { - visConfig: JSON.stringify(visConfig), - }); - - const ast = buildExpression([esaggs, tilemap]); - - return ast.toAst(); -}; diff --git a/src/plugins/tile_map/public/tooltip_formatter.js b/src/plugins/tile_map/public/tooltip_formatter.js deleted file mode 100644 index 28a08d60b0be9..0000000000000 --- a/src/plugins/tile_map/public/tooltip_formatter.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -export function tooltipFormatter(metricTitle, metricFormat, feature) { - if (!feature) { - return ''; - } - - return [ - { - label: metricTitle, - value: metricFormat(feature.properties.value), - }, - { - label: i18n.translate('tileMap.tooltipFormatter.latitudeLabel', { - defaultMessage: 'Latitude', - }), - value: feature.geometry.coordinates[1], - }, - { - label: i18n.translate('tileMap.tooltipFormatter.longitudeLabel', { - defaultMessage: 'Longitude', - }), - value: feature.geometry.coordinates[0], - }, - ]; -} diff --git a/src/plugins/tile_map/public/types.ts b/src/plugins/tile_map/public/types.ts deleted file mode 100644 index 89aa27c36b659..0000000000000 --- a/src/plugins/tile_map/public/types.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FeatureCollection } from 'geojson'; -import type { SchemaConfig } from 'src/plugins/visualizations/public'; -import type { DatatableColumnMeta } from 'src/plugins/expressions'; -import type { WMSOptions } from 'src/plugins/maps_legacy/public'; -import type { MapTypes } from './utils/map_types'; - -export interface TileMapVisData { - featureCollection: FeatureCollection; - meta: { - min: number; - max: number; - geohash?: DatatableColumnMeta; - geohashPrecision: number | undefined; - geohashGridDimensionsAtEquator: [number, number] | undefined; - }; -} - -export interface TileMapVisDimensions { - metric: SchemaConfig; - geohash: SchemaConfig | null; - geocentroid: SchemaConfig | null; -} - -export interface TileMapVisParams { - colorSchema: string; - mapType: MapTypes; - isDesaturated: boolean; - addTooltip: boolean; - heatClusterSize: number; - legendPosition: 'bottomright' | 'bottomleft' | 'topright' | 'topleft'; - mapZoom: number; - mapCenter: [number, number]; - wms: WMSOptions; -} - -export interface TileMapVisConfig extends TileMapVisParams { - dimensions: TileMapVisDimensions; -} diff --git a/src/plugins/tile_map/public/utils/convert_to_geojson.ts b/src/plugins/tile_map/public/utils/convert_to_geojson.ts deleted file mode 100644 index 57ece8d5ccd47..0000000000000 --- a/src/plugins/tile_map/public/utils/convert_to_geojson.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Feature } from 'geojson'; -import type { Datatable } from '../../../expressions/public'; -import type { TileMapVisDimensions, TileMapVisData } from '../types'; -import { decodeGeoHash } from './decode_geo_hash'; -import { gridDimensions } from './grid_dimensions'; - -export function convertToGeoJson( - tabifiedResponse: Datatable, - { geohash, geocentroid, metric }: TileMapVisDimensions -): TileMapVisData { - let features: Feature[]; - let min = Infinity; - let max = -Infinity; - - if (tabifiedResponse && tabifiedResponse.rows) { - const table = tabifiedResponse; - const geohashColumn = geohash ? table.columns[geohash.accessor] : null; - - if (!geohashColumn) { - features = []; - } else { - const metricColumn = table.columns[metric.accessor]; - const geocentroidColumn = geocentroid ? table.columns[geocentroid.accessor] : null; - - features = table.rows - .map((row) => { - const geohashValue = row[geohashColumn.id]; - if (!geohashValue) return false; - const geohashLocation = decodeGeoHash(geohashValue); - - let pointCoordinates: number[]; - if (geocentroidColumn) { - const location = row[geocentroidColumn.id]; - pointCoordinates = [location.lon, location.lat]; - } else { - pointCoordinates = [geohashLocation.longitude[2], geohashLocation.latitude[2]]; - } - - const rectangle = [ - [geohashLocation.latitude[0], geohashLocation.longitude[0]], - [geohashLocation.latitude[0], geohashLocation.longitude[1]], - [geohashLocation.latitude[1], geohashLocation.longitude[1]], - [geohashLocation.latitude[1], geohashLocation.longitude[0]], - ]; - - const centerLatLng = [geohashLocation.latitude[2], geohashLocation.longitude[2]]; - - if (geohash?.params.useGeocentroid) { - // see https://github.com/elastic/elasticsearch/issues/24694 for why clampGrid is used - pointCoordinates[0] = clampGrid( - pointCoordinates[0], - geohashLocation.longitude[0], - geohashLocation.longitude[1] - ); - pointCoordinates[1] = clampGrid( - pointCoordinates[1], - geohashLocation.latitude[0], - geohashLocation.latitude[1] - ); - } - - const value = row[metricColumn.id]; - min = Math.min(min, value); - max = Math.max(max, value); - - return { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: pointCoordinates, - }, - properties: { - geohash: geohashValue, - geohash_meta: { - center: centerLatLng, - rectangle, - }, - value, - }, - } as Feature; - }) - .filter((row): row is Feature => !!row); - } - } else { - features = []; - } - - const convertedData: TileMapVisData = { - featureCollection: { - type: 'FeatureCollection', - features, - }, - meta: { - min, - max, - geohashPrecision: geohash?.params.precision, - geohashGridDimensionsAtEquator: geohash?.params.precision - ? gridDimensions(geohash.params.precision) - : undefined, - }, - }; - - if (geohash && geohash.accessor) { - convertedData.meta.geohash = tabifiedResponse.columns[geohash.accessor].meta; - } - - return convertedData; -} - -function clampGrid(val: number, min: number, max: number) { - if (val > max) val = max; - else if (val < min) val = min; - return val; -} diff --git a/src/plugins/tile_map/public/utils/decode_geo_hash.test.ts b/src/plugins/tile_map/public/utils/decode_geo_hash.test.ts deleted file mode 100644 index 8a62a6f7d6a39..0000000000000 --- a/src/plugins/tile_map/public/utils/decode_geo_hash.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { decodeGeoHash } from './decode_geo_hash'; - -test('decodeGeoHash', () => { - expect(decodeGeoHash('drm3btev3e86')).toEqual({ - latitude: [41.119999922811985, 41.12000009045005, 41.12000000663102], - longitude: [-71.34000029414892, -71.3399999588728, -71.34000012651086], - }); -}); diff --git a/src/plugins/tile_map/public/utils/decode_geo_hash.ts b/src/plugins/tile_map/public/utils/decode_geo_hash.ts deleted file mode 100644 index bbabfd007d5c8..0000000000000 --- a/src/plugins/tile_map/public/utils/decode_geo_hash.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -interface DecodedGeoHash { - latitude: number[]; - longitude: number[]; -} - -/** - * Decodes geohash to object containing - * top-left and bottom-right corners of - * rectangle and center point. - */ -export function decodeGeoHash(geohash: string): DecodedGeoHash { - const BITS: number[] = [16, 8, 4, 2, 1]; - const BASE32: string = '0123456789bcdefghjkmnpqrstuvwxyz'; - let isEven: boolean = true; - const lat: number[] = []; - const lon: number[] = []; - lat[0] = -90.0; - lat[1] = 90.0; - lon[0] = -180.0; - lon[1] = 180.0; - let latErr: number = 90.0; - let lonErr: number = 180.0; - [...geohash].forEach((nextChar: string) => { - const cd: number = BASE32.indexOf(nextChar); - for (let j = 0; j < 5; j++) { - const mask: number = BITS[j]; - if (isEven) { - lonErr = lonErr /= 2; - refineInterval(lon, cd, mask); - } else { - latErr = latErr /= 2; - refineInterval(lat, cd, mask); - } - isEven = !isEven; - } - }); - lat[2] = (lat[0] + lat[1]) / 2; - lon[2] = (lon[0] + lon[1]) / 2; - - return { - latitude: lat, - longitude: lon, - }; -} - -function refineInterval(interval: number[], cd: number, mask: number) { - if (cd & mask) { /* eslint-disable-line */ - interval[0] = (interval[0] + interval[1]) / 2; - } else { - interval[1] = (interval[0] + interval[1]) / 2; - } -} - -interface GeoBoundingBoxCoordinate { - lat: number; - lon: number; -} - -interface GeoBoundingBox { - top_left: GeoBoundingBoxCoordinate; - bottom_right: GeoBoundingBoxCoordinate; -} - -export function geoContains(collar?: GeoBoundingBox, bounds?: GeoBoundingBox) { - if (!bounds || !collar) return false; - // test if bounds top_left is outside collar - if (bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) { - return false; - } - - // test if bounds bottom_right is outside collar - if ( - bounds.bottom_right.lat < collar.bottom_right.lat || - bounds.bottom_right.lon > collar.bottom_right.lon - ) { - return false; - } - - // both corners are inside collar so collar contains bounds - return true; -} diff --git a/src/plugins/tile_map/public/utils/grid_dimensions.ts b/src/plugins/tile_map/public/utils/grid_dimensions.ts deleted file mode 100644 index 346d42227cd24..0000000000000 --- a/src/plugins/tile_map/public/utils/grid_dimensions.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// geohash precision mapping of geohash grid cell dimensions (width x height, in meters) at equator. -// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator -const gridAtEquator: { [key: number]: [number, number] } = { - 1: [5009400, 4992600], - 2: [1252300, 624100], - 3: [156500, 156000], - 4: [39100, 19500], - 5: [4900, 4900], - 6: [1200, 609.4], - 7: [152.9, 152.4], - 8: [38.2, 19], - 9: [4.8, 4.8], - 10: [1.2, 0.595], - 11: [0.149, 0.149], - 12: [0.037, 0.019], -}; - -export function gridDimensions(precision: number) { - return gridAtEquator[precision]; -} diff --git a/src/plugins/tile_map/public/utils/index.ts b/src/plugins/tile_map/public/utils/index.ts deleted file mode 100644 index a89f1e9d53764..0000000000000 --- a/src/plugins/tile_map/public/utils/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { convertToGeoJson } from './convert_to_geojson'; -export { geoContains } from './decode_geo_hash'; diff --git a/src/plugins/tile_map/public/utils/map_types.ts b/src/plugins/tile_map/public/utils/map_types.ts deleted file mode 100644 index afcc8e02ac963..0000000000000 --- a/src/plugins/tile_map/public/utils/map_types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export enum MapTypes { - ScaledCircleMarkers = 'Scaled Circle Markers', - ShadedCircleMarkers = 'Shaded Circle Markers', - ShadedGeohashGrid = 'Shaded Geohash Grid', - Heatmap = 'Heatmap', -} diff --git a/src/plugins/tile_map/tsconfig.json b/src/plugins/tile_map/tsconfig.json deleted file mode 100644 index fec191402f2ab..0000000000000 --- a/src/plugins/tile_map/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": ["public/**/*", "server/**/*"], - "references": [ - { "path": "../maps_legacy/tsconfig.json" }, - { "path": "../maps_ems/tsconfig.json" }, - { "path": "../vis_default_editor/tsconfig.json" }, - ] -} diff --git a/src/plugins/ui_actions/kibana.json b/src/plugins/ui_actions/kibana.json index ca979aa021026..d112f6310a1fe 100644 --- a/src/plugins/ui_actions/kibana.json +++ b/src/plugins/ui_actions/kibana.json @@ -3,8 +3,9 @@ "version": "kibana", "server": false, "ui": true, - "requiredBundles": [ - "kibanaUtils", - "kibanaReact" - ] + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/url_forwarding/kibana.json b/src/plugins/url_forwarding/kibana.json index 4f534c1219b34..253466631f2e2 100644 --- a/src/plugins/url_forwarding/kibana.json +++ b/src/plugins/url_forwarding/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "Kibana App", + "githubTeam": "kibana-app" + }, "requiredPlugins": ["kibanaLegacy"] } diff --git a/src/plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts b/src/plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts index bfb0cbc380011..febedd49621fd 100644 --- a/src/plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts +++ b/src/plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts @@ -7,6 +7,7 @@ */ import { useCallback } from 'react'; +import type { SerializableRecord } from '@kbn/utility-types'; import { IAggConfig, AggParamType } from 'src/plugins/data/public'; @@ -20,7 +21,7 @@ function useSubAggParamsHandlers( ) { const setAggParamValue = useCallback( (aggId, paramName, val) => { - const parsedParams = subAgg.toJSON(); + const parsedParams = subAgg.serialize(); const params = { ...parsedParams, params: { @@ -36,10 +37,18 @@ function useSubAggParamsHandlers( const onAggTypeChange = useCallback( (aggId, aggType) => { - const parsedAgg = subAgg.toJSON(); + const parsedAgg = subAgg.serialize(); + const parsedAggParams = parsedAgg.params as SerializableRecord; + // we should share between aggs only field and base params: json, label, time shift. const params = { ...parsedAgg, + params: { + field: parsedAggParams.field, + json: parsedAggParams.json, + customLabel: parsedAggParams.customLabel, + timeShift: parsedAggParams.timeShift, + }, type: aggType, }; diff --git a/src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts b/src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts index 3b07743e79f45..41fa00bbe2386 100644 --- a/src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts @@ -485,7 +485,7 @@ export const samplePieVis = { }, { count: 0, - script: "doc['timestamp'].value.hourOfDay", + script: "doc['timestamp'].value.getHour()", lang: 'painless', name: 'hour_of_day', type: 'number', @@ -502,7 +502,7 @@ export const samplePieVis = { title: 'kibana_sample_data_flights', timeFieldName: 'timestamp', fields: - '[{"count":0,"name":"AvgTicketPrice","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Cancelled","type":"boolean","esTypes":["boolean"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Carrier","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Dest","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestAirportID","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestCityName","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestCountry","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestLocation","type":"geo_point","esTypes":["geo_point"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestRegion","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestWeather","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DistanceKilometers","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DistanceMiles","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightDelay","type":"boolean","esTypes":["boolean"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightDelayMin","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightDelayType","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightNum","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightTimeHour","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightTimeMin","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Origin","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginAirportID","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginCityName","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginCountry","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginLocation","type":"geo_point","esTypes":["geo_point"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginRegion","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginWeather","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"_id","type":"string","esTypes":["_id"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_index","type":"string","esTypes":["_index"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_score","type":"number","scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_source","type":"_source","esTypes":["_source"],"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_type","type":"string","esTypes":["_type"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"dayOfWeek","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"timestamp","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"script":"doc[\'timestamp\'].value.hourOfDay","lang":"painless","name":"hour_of_day","type":"number","scripted":true,"searchable":true,"aggregatable":true,"readFromDocValues":false}]', + '[{"count":0,"name":"AvgTicketPrice","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Cancelled","type":"boolean","esTypes":["boolean"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Carrier","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Dest","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestAirportID","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestCityName","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestCountry","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestLocation","type":"geo_point","esTypes":["geo_point"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestRegion","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DestWeather","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DistanceKilometers","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"DistanceMiles","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightDelay","type":"boolean","esTypes":["boolean"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightDelayMin","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightDelayType","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightNum","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightTimeHour","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"FlightTimeMin","type":"number","esTypes":["float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"Origin","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginAirportID","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginCityName","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginCountry","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginLocation","type":"geo_point","esTypes":["geo_point"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginRegion","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"OriginWeather","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"_id","type":"string","esTypes":["_id"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_index","type":"string","esTypes":["_index"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_score","type":"number","scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_source","type":"_source","esTypes":["_source"],"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_type","type":"string","esTypes":["_type"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"dayOfWeek","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"timestamp","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"script":"doc[\'timestamp\'].value.getHour()","lang":"painless","name":"hour_of_day","type":"number","scripted":true,"searchable":true,"aggregatable":true,"readFromDocValues":false}]', fieldFormatMap: '{"AvgTicketPrice":{"id":"number","params":{"parsedUrl":{"origin":"http://localhost:5801","pathname":"/app/visualize","basePath":""},"pattern":"$0,0.[00]"}},"hour_of_day":{"id":"number","params":{"parsedUrl":{"origin":"http://localhost:5801","pathname":"/app/visualize","basePath":""},"pattern":"00"}}}', }, diff --git a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js index b9aa70f5207af..27622e29c2061 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/markdown_editor.js @@ -26,15 +26,17 @@ export class MarkdownEditor extends Component { this.props.onChange({ markdown: value }); }; - handleOnLoad = (ace) => { - this.ace = ace; + handleOnLoad = (editor) => { + this.editor = editor; }; - handleVarClick(snippet) { - return () => { - if (this.ace) this.ace.insert(snippet); - }; - } + handleVarClick = (snippet) => () => { + if (this.editor) { + const range = this.editor.getSelection(); + + this.editor.executeEdits('', [{ range, text: snippet }]); + } + }; render() { const { visData, model, getConfig } = this.props; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx index 4faa0110b9711..152ae43bebd64 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx @@ -8,7 +8,6 @@ import React, { Component } from 'react'; import * as Rx from 'rxjs'; -import uuid from 'uuid/v4'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; import { EventEmitter } from 'events'; @@ -64,6 +63,7 @@ export class VisEditor extends Component +): Vis => { + const doReplace = ( + obj: Partial<{ + id: string | (() => string); + }> + ) => { + if (typeof obj?.id === 'function') { + obj.id = obj.id(); + } + }; + + doReplace(vis.params); + + vis.params.series?.forEach((series) => { + doReplace(series); + series.metrics?.forEach((metric) => doReplace(metric)); + }); + + return vis; +}; + +export const metricsVisDefinition: VisTypeDefinition< + TimeseriesVisParams | TimeseriesVisDefaultParams +> = { name: 'metrics', title: i18n.translate('visTypeTimeseries.kbnVisTypes.metricsTitle', { defaultMessage: 'TSVB' }), description: i18n.translate('visTypeTimeseries.kbnVisTypes.metricsDescription', { @@ -26,11 +58,11 @@ export const metricsVisDefinition = { group: VisGroups.PROMOTED, visConfig: { defaults: { - id: uuid(), + id: () => uuid(), type: PANEL_TYPES.TIMESERIES, series: [ { - id: uuid(), + id: () => uuid(), color: TSVB_DEFAULT_COLOR, split_mode: 'everything', palette: { @@ -39,7 +71,7 @@ export const metricsVisDefinition = { }, metrics: [ { - id: uuid(), + id: () => uuid(), type: 'count', }, ], @@ -66,6 +98,7 @@ export const metricsVisDefinition = { drop_last_bucket: 0, }, }, + setup: (vis) => Promise.resolve(withReplacedIds(vis)), editorConfig: { editor: TSVB_EDITOR_NAME, }, diff --git a/src/plugins/vis_type_timeseries/public/types.ts b/src/plugins/vis_type_timeseries/public/types.ts index dcde3195ba984..a1fb11eeca0b8 100644 --- a/src/plugins/vis_type_timeseries/public/types.ts +++ b/src/plugins/vis_type_timeseries/public/types.ts @@ -19,3 +19,13 @@ export type DragHandleProps = FirstArgumentOf< >['dragHandleProps']; export type TimeseriesVisParams = Panel; + +export type TimeseriesVisDefaultParams = TimeseriesVisParams & { + id: () => string; + series: { + id: () => string; + metrics: { + id: () => string; + }; + }; +}; diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index 407e20fe0688a..a579e85c0caf2 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["data", "visualizations", "mapsEms", "expressions", "inspector"], "optionalPlugins": ["home","usageCollection"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor", "esUiShared"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx index 148c630ad94e5..9150b31343799 100644 --- a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx +++ b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx @@ -7,13 +7,13 @@ */ import React, { useCallback } from 'react'; -import { EuiCodeEditor } from '@elastic/eui'; import compactStringify from 'json-stringify-pretty-compact'; import hjson from 'hjson'; import 'brace/mode/hjson'; import { i18n } from '@kbn/i18n'; import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { EuiCodeEditor } from '../../../es_ui_shared/public'; import { getNotifications } from '../services'; import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 95b3b573a6bfa..bc7d72c042841 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -273,10 +273,8 @@ The URL is an identifier only. Kibana and your browser will never access this UR vegaLogger.warn = this._onWarning.bind(this); this.spec = compile(this.vlspec as TopLevelSpec, { logger: vegaLogger }).spec; - // When using VL with the type=map and user did not provid their own projection settings, + // When using Vega-Lite (VL) with the type=map and user did not provid their own projection settings, // remove the default projection that was generated by VegaLite compiler. - // This way we let leaflet-vega library inject a different default projection for tile maps. - // Also, VL injects default padding and autosize values, but neither should be set for vega-leaflet. if (this.useMap) { if (!this.spec || !this.vlspec) return; const hasConfig = _.isPlainObject(this.vlspec.config); diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index e1b8b5d9d4bac..62bdd0262b4a5 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -26,5 +26,6 @@ { "path": "../kibana_utils/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, { "path": "../vis_default_editor/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, ] } diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts index f23d9e4ada336..d5e1360ced74c 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts @@ -76,7 +76,7 @@ export const getAggs = () => { title: 'kibana_sample_data_flights', timeFieldName: 'timestamp', fields: - '[{"count":0,"script":"doc[\'timestamp\'].value.hourOfDay","lang":"painless","name":"hour_of_day","type":"number","scripted":true,"searchable":true,"aggregatable":true,"readFromDocValues":false}]', + '[{"count":0,"script":"doc[\'timestamp\'].value.getHour()","lang":"painless","name":"hour_of_day","type":"number","scripted":true,"searchable":true,"aggregatable":true,"readFromDocValues":false}]', fieldFormatMap: '{"hour_of_day":{"id":"number","params":{"pattern":"00"}},"AvgTicketPrice":{"id":"number","params":{"pattern":"$0,0.[00]"}}}', runtimeFieldMap: '{}', @@ -241,7 +241,7 @@ export const getVis = (bucketType: string) => { title: 'kibana_sample_data_flights', timeFieldName: 'timestamp', fields: - '[{"count":0,"script":"doc[\'timestamp\'].value.hourOfDay","lang":"painless","name":"hour_of_day","type":"number","scripted":true,"searchable":true,"aggregatable":true,"readFromDocValues":false}]', + '[{"count":0,"script":"doc[\'timestamp\'].value.getHour()","lang":"painless","name":"hour_of_day","type":"number","scripted":true,"searchable":true,"aggregatable":true,"readFromDocValues":false}]', fieldFormatMap: '{"hour_of_day":{"id":"number","params":{"pattern":"00"}},"AvgTicketPrice":{"id":"number","params":{"pattern":"$0,0.[00]"}}}', runtimeFieldMap: '{}', diff --git a/test/common/config.js b/test/common/config.js index 5b5d01cfeb1e4..eb110fad55ea8 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -41,7 +41,6 @@ export default function () { )}`, `--elasticsearch.username=${kibanaServerTestUser.username}`, `--elasticsearch.password=${kibanaServerTestUser.password}`, - `--home.disableWelcomeScreen=true`, // Needed for async search functional tests to introduce a delay `--data.search.aggs.shardDelay.enabled=true`, `--security.showInsecureClusterWarning=false`, diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/dashboard_state.ts index ea2031f370eba..1a9cf3b7593a0 100644 --- a/test/functional/apps/dashboard/dashboard_state.ts +++ b/test/functional/apps/dashboard/dashboard_state.ts @@ -18,7 +18,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visualize', 'header', 'discover', - 'tileMap', 'visChart', 'share', 'timePicker', @@ -27,11 +26,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const queryBar = getService('queryBar'); const pieChart = getService('pieChart'); - const inspector = getService('inspector'); const retry = getService('retry'); const elasticChart = getService('elasticChart'); const kibanaServer = getService('kibanaServer'); - const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardAddPanel = getService('dashboardAddPanel'); const enableNewChartLibraryDebug = async () => { @@ -166,38 +163,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(headers.length).to.be(0); }); - it('Tile map with no changes will update with visualization changes', async () => { - await PageObjects.dashboard.gotoDashboardLandingPage(); - - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.timePicker.setHistoricalDataRange(); - - await dashboardAddPanel.addVisualization('Visualization TileMap'); - await PageObjects.dashboard.saveDashboard('No local edits'); - - await dashboardPanelActions.openInspector(); - const tileMapData = await inspector.getTableData(); - await inspector.close(); - - await PageObjects.dashboard.switchToEditMode(); - await dashboardPanelActions.openContextMenu(); - await dashboardPanelActions.clickEdit(); - - await PageObjects.tileMap.clickMapZoomIn(); - await PageObjects.tileMap.clickMapZoomIn(); - await PageObjects.tileMap.clickMapZoomIn(); - await PageObjects.tileMap.clickMapZoomIn(); - - await PageObjects.visualize.saveVisualizationExpectSuccess('Visualization TileMap'); - - await PageObjects.header.clickDashboard(); - - await dashboardPanelActions.openInspector(); - const changedTileMapData = await inspector.getTableData(); - await inspector.close(); - expect(changedTileMapData.length).to.not.equal(tileMapData.length); - }); - const getUrlFromShare = async () => { await PageObjects.share.clickShareTopNavButton(); const sharedUrl = await PageObjects.share.getSharedUrl(); diff --git a/test/functional/apps/dashboard/embeddable_library.ts b/test/functional/apps/dashboard/embeddable_library.ts index d66f6e834c367..fd1aa0d91def7 100644 --- a/test/functional/apps/dashboard/embeddable_library.ts +++ b/test/functional/apps/dashboard/embeddable_library.ts @@ -68,44 +68,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); expect(libraryActionExists).to.be(true); }); - - it('unlink map panel from embeddable library', async () => { - // add map panel from library - await dashboardAddPanel.clickOpenAddPanel(); - await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); - await find.clickByButtonText('Rendering Test: geo map'); - await dashboardAddPanel.closeAddPanel(); - - const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); - await panelActions.unlinkFromLibary(originalPanel); - await testSubjects.existOrFail('unlinkPanelSuccess'); - - const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); - const libraryActionExists = await testSubjects.descendantExists( - 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', - updatedPanel - ); - expect(libraryActionExists).to.be(false); - - await dashboardAddPanel.clickOpenAddPanel(); - await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); - await find.existsByLinkText('Rendering Test: geo map'); - await dashboardAddPanel.closeAddPanel(); - }); - - it('save map panel to embeddable library', async () => { - const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); - await panelActions.saveToLibrary('Rendering Test: geo map - copy', originalPanel); - await testSubjects.existOrFail('addPanelToLibrarySuccess'); - - const updatedPanel = await testSubjects.find( - 'embeddablePanelHeading-RenderingTest:geomap-copy' - ); - const libraryActionExists = await testSubjects.descendantExists( - 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', - updatedPanel - ); - expect(libraryActionExists).to.be(true); - }); }); } diff --git a/test/functional/apps/home/_welcome.ts b/test/functional/apps/home/_welcome.ts new file mode 100644 index 0000000000000..ec7e9759558df --- /dev/null +++ b/test/functional/apps/home/_welcome.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'home']); + + describe('Welcome interstitial', () => { + before(async () => { + // Need to navigate to page first to clear storage before test can be run + await PageObjects.common.navigateToUrl('home', undefined); + await browser.clearLocalStorage(); + await esArchiver.emptyKibanaIndex(); + }); + + it('is displayed on a fresh on-prem install', async () => { + await PageObjects.common.navigateToUrl('home', undefined, { disableWelcomePrompt: false }); + expect(await PageObjects.home.isWelcomeInterstitialDisplayed()).to.be(true); + }); + }); +} diff --git a/test/functional/apps/home/index.js b/test/functional/apps/home/index.js index ff6e522e41639..257ee724f6c8b 100644 --- a/test/functional/apps/home/index.js +++ b/test/functional/apps/home/index.js @@ -21,5 +21,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_newsfeed')); loadTestFile(require.resolve('./_add_data')); loadTestFile(require.resolve('./_sample_data')); + loadTestFile(require.resolve('./_welcome')); }); } diff --git a/test/functional/apps/visualize/_region_map.ts b/test/functional/apps/visualize/_region_map.ts deleted file mode 100644 index 916e8dbaee3a0..0000000000000 --- a/test/functional/apps/visualize/_region_map.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - describe('vector map', function () { - const inspector = getService('inspector'); - const log = getService('log'); - const find = getService('find'); - const PageObjects = getPageObjects(['visualize', 'visEditor', 'timePicker']); - - before(async function () { - await PageObjects.visualize.initTests(); - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - log.debug('clickRegionMap'); - await PageObjects.visualize.clickRegionMap(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - log.debug('Bucket = Shape field'); - await PageObjects.visEditor.clickBucket('Shape field'); - log.debug('Aggregation = Terms'); - await PageObjects.visEditor.selectAggregation('Terms'); - log.debug('Field = geo.src'); - await PageObjects.visEditor.selectField('geo.src'); - await PageObjects.visEditor.clickGo(); - }); - - describe('vector map', function indexPatternCreation() { - it('should have inspector enabled', async function () { - await inspector.expectIsEnabled(); - }); - - it('should show results after clicking play (join on states)', async function () { - const expectedData = [ - ['CN', '2,592'], - ['IN', '2,373'], - ['US', '1,194'], - ['ID', '489'], - ['BR', '415'], - ]; - await inspector.open(); - await inspector.expectTableData(expectedData); - await inspector.close(); - }); - - it('should change results after changing layer to world', async function () { - await PageObjects.visEditor.clickOptionsTab(); - await PageObjects.visEditor.setSelectByOptionText( - 'regionMapOptionsSelectLayer', - 'World Countries' - ); - - // ensure all fields are there - await PageObjects.visEditor.setSelectByOptionText( - 'regionMapOptionsSelectJoinField', - 'ISO 3166-1 alpha-2 code' - ); - await PageObjects.visEditor.setSelectByOptionText( - 'regionMapOptionsSelectJoinField', - 'ISO 3166-1 alpha-3 code' - ); - await PageObjects.visEditor.setSelectByOptionText( - 'regionMapOptionsSelectJoinField', - 'name' - ); - await PageObjects.visEditor.setSelectByOptionText( - 'regionMapOptionsSelectJoinField', - 'ISO 3166-1 alpha-2 code' - ); - - await inspector.open(); - const actualData = await inspector.getTableData(); - const expectedData = [ - ['CN', '2,592'], - ['IN', '2,373'], - ['US', '1,194'], - ['ID', '489'], - ['BR', '415'], - ]; - expect(actualData).to.eql(expectedData); - - await inspector.close(); - }); - - it('should contain a dropdown with the default road_map base layer as an option', async () => { - const selectField = await find.byCssSelector('#wmsOptionsSelectTmsLayer'); - const $ = await selectField.parseDomContent(); - const optionsText = $('option') - .toArray() - .map((option) => $(option).text()); - - expect(optionsText.includes('road_map')).to.be(true); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_tile_map.ts b/test/functional/apps/visualize/_tile_map.ts deleted file mode 100644 index 812b6a7d86802..0000000000000 --- a/test/functional/apps/visualize/_tile_map.ts +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import expect from '@kbn/expect'; - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const inspector = getService('inspector'); - const filterBar = getService('filterBar'); - const browser = getService('browser'); - const PageObjects = getPageObjects([ - 'common', - 'visualize', - 'visEditor', - 'visChart', - 'timePicker', - 'tileMap', - ]); - - describe('tile map visualize app', function () { - describe('incomplete config', function describeIndexTests() { - before(async function () { - await PageObjects.visualize.initTests(); - await browser.setWindowSize(1280, 1000); - - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - log.debug('clickTileMap'); - await PageObjects.visualize.clickTileMap(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - // do not configure aggs - }); - - it('should be able to zoom in twice', async () => { - // should not throw - await PageObjects.tileMap.clickMapZoomIn(); - await PageObjects.tileMap.clickMapZoomIn(); - }); - }); - - describe('complete config', function describeIndexTests() { - before(async function () { - await browser.setWindowSize(1280, 1000); - - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - log.debug('clickTileMap'); - await PageObjects.visualize.clickTileMap(); - await PageObjects.visualize.clickNewSearch(); - await PageObjects.timePicker.setDefaultAbsoluteRange(); - log.debug('select bucket Geo Coordinates'); - await PageObjects.visEditor.clickBucket('Geo coordinates'); - log.debug('Click aggregation Geohash'); - await PageObjects.visEditor.selectAggregation('Geohash'); - log.debug('Click field geo.coordinates'); - await retry.try(async function tryingForTime() { - await PageObjects.visEditor.selectField('geo.coordinates'); - }); - await PageObjects.visEditor.clickGo(); - }); - - type SampleTableData = Array; - - /** - * manually compare data due to possible small difference in numbers. This is browser dependent. - */ - function compareTableData(actual: string[][], expected: SampleTableData[]) { - log.debug('comparing expected: ', expected); - log.debug('with actual: ', actual); - - const roundedValues = actual.map((row) => { - // Parse last element in each row as JSON and floor the lat/long value - const coords = JSON.parse(row[row.length - 1]); - return [ - ...row.slice(0, -1), - { - lat: Math.floor(parseFloat(coords.lat)), - lon: Math.floor(parseFloat(coords.lon)), - }, - ]; - }); - - expect(roundedValues).to.eql(expected); - } - - describe('tile map chart', function indexPatternCreation() { - it('should have inspector enabled', async function () { - await inspector.expectIsEnabled(); - }); - - it('should show correct tile map data on default zoom level', async function () { - const expectedTableData = [ - ['-', '9', '5,787', { lat: 37, lon: -104 }], - ['-', 'd', '5,600', { lat: 37, lon: -82 }], - ['-', 'c', '1,319', { lat: 47, lon: -110 }], - ['-', 'b', '999', { lat: 62, lon: -156 }], - ['-', 'f', '187', { lat: 45, lon: -83 }], - ['-', '8', '108', { lat: 18, lon: -157 }], - ]; - // level 1 - await PageObjects.tileMap.clickMapZoomOut(); - // level 0 - await PageObjects.tileMap.clickMapZoomOut(); - - await inspector.open(); - await inspector.setTablePageSize(50); - const actualTableData = await inspector.getTableData(); - await inspector.close(); - compareTableData(actualTableData, expectedTableData); - }); - - it('should not be able to zoom out beyond 0', async function () { - await PageObjects.tileMap.zoomAllTheWayOut(); - const enabled = await PageObjects.tileMap.getMapZoomOutEnabled(); - expect(enabled).to.be(false); - }); - - it('Fit data bounds should zoom to level 3', async function () { - const expectedPrecision2DataTable = [ - ['-', 'dn', '1,429', { lat: 36, lon: -85 }], - ['-', 'dp', '1,418', { lat: 41, lon: -85 }], - ['-', '9y', '1,215', { lat: 36, lon: -96 }], - ['-', '9z', '1,099', { lat: 42, lon: -96 }], - ['-', 'dr', '1,076', { lat: 42, lon: -74 }], - ['-', 'dj', '982', { lat: 31, lon: -85 }], - ['-', '9v', '938', { lat: 31, lon: -96 }], - ['-', '9q', '722', { lat: 36, lon: -120 }], - ['-', '9w', '475', { lat: 36, lon: -107 }], - ['-', 'cb', '457', { lat: 46, lon: -96 }], - ['-', 'c2', '453', { lat: 47, lon: -120 }], - ['-', '9x', '420', { lat: 41, lon: -107 }], - ['-', 'dq', '399', { lat: 37, lon: -78 }], - ['-', '9r', '396', { lat: 41, lon: -120 }], - ['-', '9t', '274', { lat: 32, lon: -107 }], - ['-', 'c8', '271', { lat: 47, lon: -107 }], - ['-', 'dh', '214', { lat: 26, lon: -82 }], - ['-', 'b6', '207', { lat: 60, lon: -162 }], - ['-', 'bd', '206', { lat: 59, lon: -153 }], - ['-', 'b7', '167', { lat: 64, lon: -163 }], - ]; - - await PageObjects.tileMap.clickMapFitDataBounds(); - await inspector.open(); - const data = await inspector.getTableData(); - await inspector.close(); - compareTableData(data, expectedPrecision2DataTable); - }); - - it('Fit data bounds works with pinned filter data', async () => { - const expectedPrecision2DataTable = [ - ['-', 'f05', '1', { lat: 45, lon: -85 }], - ['-', 'dpr', '1', { lat: 40, lon: -79 }], - ['-', '9qh', '1', { lat: 33, lon: -118 }], - ]; - - await filterBar.addFilter('bytes', 'is between', '19980', '19990'); - await filterBar.toggleFilterPinned('bytes'); - await PageObjects.tileMap.zoomAllTheWayOut(); - await PageObjects.tileMap.clickMapFitDataBounds(); - - await inspector.open(); - const data = await inspector.getTableData(); - await inspector.close(); - - await filterBar.removeAllFilters(); - compareTableData(data, expectedPrecision2DataTable); - }); - - it('Newly saved visualization retains map bounds', async () => { - const vizName1 = 'Visualization TileMap'; - - await PageObjects.tileMap.clickMapZoomIn(); - await PageObjects.tileMap.clickMapZoomIn(); - - const mapBounds = await PageObjects.tileMap.getMapBounds(); - await inspector.close(); - - await PageObjects.visualize.saveVisualizationExpectSuccess(vizName1); - - const afterSaveMapBounds = await PageObjects.tileMap.getMapBounds(); - - await inspector.close(); - // For some reason the values are slightly different, so we can't check that they are equal. But we did - // have a bug where after the save, there were _no_ map bounds. So this checks for the later case, but - // until we figure out how to make sure the map center is always the exact same, we can't comparison check. - expect(mapBounds).to.not.be(undefined); - expect(afterSaveMapBounds).to.not.be(undefined); - }); - }); - - describe('Only request data around extent of map option', () => { - it('when checked adds filters to aggregation', async () => { - const vizName1 = 'Visualization TileMap'; - await PageObjects.visualize.loadSavedVisualization(vizName1); - await inspector.open(); - await inspector.expectTableHeaders(['Filter', 'Geohash', 'Count', 'Geo Centroid']); - await inspector.close(); - }); - - it('when not checked does not add filters to aggregation', async () => { - await PageObjects.visEditor.toggleOpenEditor(2); - await PageObjects.visEditor.setIsFilteredByCollarCheckbox(false); - await PageObjects.visEditor.clickGo(); - await inspector.open(); - await inspector.expectTableHeaders(['Geohash', 'Count', 'Geo Centroid']); - await inspector.close(); - }); - - after(async () => { - await PageObjects.visEditor.setIsFilteredByCollarCheckbox(true); - await PageObjects.visEditor.clickGo(); - }); - }); - }); - }); -} diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts index 89db60bc7645c..b8b74d5cd7bf3 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/_tsvb_markdown.ts @@ -11,10 +11,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { visualBuilder, timePicker, visualize } = getPageObjects([ + const { visualBuilder, timePicker, visualize, visChart } = getPageObjects([ 'visualBuilder', 'timePicker', 'visualize', + 'visChart', ]); const retry = getService('retry'); @@ -76,6 +77,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(markdownText).to.be(html); }); + it('markdown variables should be clickable', async () => { + await visualBuilder.clearMarkdown(); + const [firstVariable] = await visualBuilder.getMarkdownTableVariables(); + await firstVariable.selector.click(); + await visChart.waitForVisualizationRenderingStabilized(); + const markdownText = await visualBuilder.getMarkdownText(); + expect(markdownText).to.be('46'); + }); + it('should render mustache list', async () => { const list = '{{#each _all}}\n{{ data.formatted.[0] }} {{ data.raw.[0] }}\n{{/each}}'; const expectedRenderer = 'Sep 22, 2015 @ 06:00:00.000,6 1442901600000,6'; diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 4af871bd9347d..9004ecaf22d80 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -91,8 +91,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_linked_saved_searches')); loadTestFile(require.resolve('./_visualize_listing')); loadTestFile(require.resolve('./_add_to_dashboard.ts')); - loadTestFile(require.resolve('./_tile_map')); - loadTestFile(require.resolve('./_region_map')); }); describe('visualize ciGroup12', function () { diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 49d56d6f43784..70589b9d9505e 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -19,6 +19,7 @@ interface NavigateProps { shouldLoginIfPrompted: boolean; useActualUrl: boolean; insertTimestamp: boolean; + disableWelcomePrompt: boolean; } export class CommonPageObject extends FtrService { private readonly log = this.ctx.getService('log'); @@ -37,11 +38,17 @@ export class CommonPageObject extends FtrService { * Logins to Kibana as default user and navigates to provided app * @param appUrl Kibana URL */ - private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) { + private async loginIfPrompted( + appUrl: string, + insertTimestamp: boolean, + disableWelcomePrompt: boolean + ) { // Disable the welcome screen. This is relevant for environments // which don't allow to use the yml setting, e.g. cloud production. // It is done here so it applies to logins but also to a login re-use. - await this.browser.setLocalStorageItem('home:welcome:show', 'false'); + if (disableWelcomePrompt) { + await this.browser.setLocalStorageItem('home:welcome:show', 'false'); + } let currentUrl = await this.browser.getCurrentUrl(); this.log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); @@ -76,6 +83,7 @@ export class CommonPageObject extends FtrService { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, + disableWelcomePrompt, useActualUrl, insertTimestamp, } = navigateProps; @@ -95,7 +103,7 @@ export class CommonPageObject extends FtrService { await alert?.accept(); const currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl, insertTimestamp) + ? await this.loginIfPrompted(appUrl, insertTimestamp, disableWelcomePrompt) : await this.browser.getCurrentUrl(); if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { @@ -117,6 +125,7 @@ export class CommonPageObject extends FtrService { basePath = '', ensureCurrentUrl = true, shouldLoginIfPrompted = true, + disableWelcomePrompt = true, useActualUrl = false, insertTimestamp = true, shouldUseHashForSubUrl = true, @@ -136,6 +145,7 @@ export class CommonPageObject extends FtrService { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, + disableWelcomePrompt, useActualUrl, insertTimestamp, }); @@ -156,6 +166,7 @@ export class CommonPageObject extends FtrService { basePath = '', ensureCurrentUrl = true, shouldLoginIfPrompted = true, + disableWelcomePrompt = true, useActualUrl = true, insertTimestamp = true, } = {} @@ -170,6 +181,7 @@ export class CommonPageObject extends FtrService { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, + disableWelcomePrompt, useActualUrl, insertTimestamp, }); @@ -202,7 +214,13 @@ export class CommonPageObject extends FtrService { async navigateToApp( appName: string, - { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} + { + basePath = '', + shouldLoginIfPrompted = true, + disableWelcomePrompt = true, + hash = '', + insertTimestamp = true, + } = {} ) { let appUrl: string; if (this.config.has(['apps', appName])) { @@ -233,7 +251,7 @@ export class CommonPageObject extends FtrService { this.log.debug('returned from get, calling refresh'); await this.browser.refresh(); let currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl, insertTimestamp) + ? await this.loginIfPrompted(appUrl, insertTimestamp, disableWelcomePrompt) : await this.browser.getCurrentUrl(); if (currentUrl.includes('app/kibana')) { diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index ea727069c927d..210c8f61b2391 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -536,7 +536,6 @@ export class DashboardPageObject extends FtrService { { name: AREA_CHART_VIS_NAME, description: 'AreaChart' }, { name: 'Visualization☺漢字 DataTable', description: 'DataTable' }, { name: LINE_CHART_VIS_NAME, description: 'LineChart' }, - { name: 'Visualization TileMap', description: 'TileMap' }, { name: 'Visualization MetricChart', description: 'MetricChart' }, ]; } diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index c318635fc8548..8929026a28122 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -30,6 +30,10 @@ export class HomePageObject extends FtrService { return !(await this.testSubjects.exists(`addSampleDataSet${id}`)); } + async isWelcomeInterstitialDisplayed() { + return await this.testSubjects.isDisplayed('homeWelcomeInterstitial'); + } + async getVisibileSolutions() { const solutionPanels = await this.testSubjects.findAll('~homSolutionPanel', 2000); const panelAttributes = await Promise.all( diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 4c9cb150eca03..cda2c7de44d3b 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -24,7 +24,6 @@ import { VisualBuilderPageObject } from './visual_builder_page'; import { VisualizePageObject } from './visualize_page'; import { VisualizeEditorPageObject } from './visualize_editor_page'; import { VisualizeChartPageObject } from './visualize_chart_page'; -import { TileMapPageObject } from './tile_map_page'; import { TimeToVisualizePageObject } from './time_to_visualize_page'; import { TagCloudPageObject } from './tag_cloud_page'; import { VegaChartPageObject } from './vega_chart_page'; @@ -52,7 +51,6 @@ export const pageObjects = { visualize: VisualizePageObject, visEditor: VisualizeEditorPageObject, visChart: VisualizeChartPageObject, - tileMap: TileMapPageObject, timeToVisualize: TimeToVisualizePageObject, tagCloud: TagCloudPageObject, vegaChart: VegaChartPageObject, diff --git a/test/functional/page_objects/tile_map_page.ts b/test/functional/page_objects/tile_map_page.ts deleted file mode 100644 index 079ca919543e2..0000000000000 --- a/test/functional/page_objects/tile_map_page.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FtrService } from '../ftr_provider_context'; - -export class TileMapPageObject extends FtrService { - private readonly find = this.ctx.getService('find'); - private readonly testSubjects = this.ctx.getService('testSubjects'); - private readonly retry = this.ctx.getService('retry'); - private readonly log = this.ctx.getService('log'); - private readonly inspector = this.ctx.getService('inspector'); - private readonly monacoEditor = this.ctx.getService('monacoEditor'); - private readonly header = this.ctx.getPageObject('header'); - - public async getZoomSelectors(zoomSelector: string) { - return await this.find.allByCssSelector(zoomSelector); - } - - public async clickMapButton(zoomSelector: string, waitForLoading?: boolean) { - await this.retry.try(async () => { - const zooms = await this.getZoomSelectors(zoomSelector); - for (let i = 0; i < zooms.length; i++) { - await zooms[i].click(); - } - if (waitForLoading) { - await this.header.waitUntilLoadingHasFinished(); - } - }); - } - - public async getVisualizationRequest() { - this.log.debug('getVisualizationRequest'); - await this.inspector.open(); - await this.testSubjects.click('inspectorViewChooser'); - await this.testSubjects.click('inspectorViewChooserRequests'); - await this.testSubjects.click('inspectorRequestDetailRequest'); - await this.find.byCssSelector('.react-monaco-editor-container'); - - return await this.monacoEditor.getCodeEditorValue(1); - } - - public async getMapBounds(): Promise { - const request = await this.getVisualizationRequest(); - const requestObject = JSON.parse(request); - - return requestObject.aggs.filter_agg.filter.geo_bounding_box['geo.coordinates']; - } - - public async clickMapZoomIn(waitForLoading = true) { - await this.clickMapButton('a.leaflet-control-zoom-in', waitForLoading); - } - - public async clickMapZoomOut(waitForLoading = true) { - await this.clickMapButton('a.leaflet-control-zoom-out', waitForLoading); - } - - public async getMapZoomEnabled(zoomSelector: string): Promise { - const zooms = await this.getZoomSelectors(zoomSelector); - const classAttributes = await Promise.all( - zooms.map(async (zoom) => await zoom.getAttribute('class')) - ); - return !classAttributes.join('').includes('leaflet-disabled'); - } - - public async zoomAllTheWayOut(): Promise { - // we can tell we're at level 1 because zoom out is disabled - return await this.retry.try(async () => { - await this.clickMapZoomOut(); - const enabled = await this.getMapZoomOutEnabled(); - // should be able to zoom more as current config has 0 as min level. - if (enabled) { - throw new Error('Not fully zoomed out yet'); - } - }); - } - - public async getMapZoomInEnabled() { - return await this.getMapZoomEnabled('a.leaflet-control-zoom-in'); - } - - public async getMapZoomOutEnabled() { - return await this.getMapZoomEnabled('a.leaflet-control-zoom-out'); - } - - public async clickMapFitDataBounds() { - return await this.clickMapButton('a.fa-crop'); - } -} diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index f648528b7f615..591cddd18a2b3 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -131,7 +131,7 @@ export class VisualBuilderPageObject extends FtrService { await this.clearMarkdown(); const input = await this.find.byCssSelector('.tvbMarkdownEditor__editor textarea'); await input.type(markdown); - await this.common.sleep(3000); + await this.visChart.waitForVisualizationRenderingStabilized(); } public async clearMarkdown() { diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 966a9d29b3264..1271fe5108f56 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -165,14 +165,6 @@ export class VisualizePageObject extends FtrService { await this.clickVisType('line'); } - public async clickRegionMap() { - await this.clickVisType('region_map'); - } - - public async hasRegionMap() { - return await this.hasVisType('region_map'); - } - public async clickMarkdownWidget() { await this.clickVisType('markdown'); } @@ -189,14 +181,6 @@ export class VisualizePageObject extends FtrService { await this.clickVisType('pie'); } - public async clickTileMap() { - await this.clickVisType('tile_map'); - } - - public async hasTileMap() { - return await this.hasVisType('tile_map'); - } - public async clickTimelion() { await this.clickVisType('timelion'); } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index d38203d5d07d3..73d92f8ff722b 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -469,6 +469,15 @@ class BrowserService extends FtrService { await this.driver.executeScript('return window.localStorage.removeItem(arguments[0]);', key); } + /** + * Clears all values in local storage for the focused window/frame. + * + * @return {Promise} + */ + public async clearLocalStorage(): Promise { + await this.driver.executeScript('return window.localStorage.clear();'); + } + /** * Clears session storage for the focused window/frame. * diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png deleted file mode 100644 index b0edb637e0047..0000000000000 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_3.png and /dev/null differ diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json deleted file mode 100644 index e011b69de2022..0000000000000 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"region_map_vis","type":"render","value":{"visConfig":{"addTooltip":true,"bucket":{"accessor":0},"colorSchema":"Yellow to Red","isDisplayWarning":true,"legendPosition":"bottomright","mapCenter":[0,0],"mapZoom":2,"metric":{"accessor":1,"format":{"id":"number"}},"outlineWeight":1,"selectedJoinField":{},"selectedLayer":{},"showAllShapes":true,"wms":{}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json deleted file mode 100644 index e011b69de2022..0000000000000 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ /dev/null @@ -1 +0,0 @@ -{"as":"region_map_vis","type":"render","value":{"visConfig":{"addTooltip":true,"bucket":{"accessor":0},"colorSchema":"Yellow to Red","isDisplayWarning":true,"legendPosition":"bottomright","mapCenter":[0,0],"mapZoom":2,"metric":{"accessor":1,"format":{"id":"number"}},"outlineWeight":1,"selectedJoinField":{},"selectedLayer":{},"showAllShapes":true,"wms":{}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.ts b/test/interpreter_functional/test_suites/run_pipeline/basic.ts index c277f12d0ff37..526373aeeaf53 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.ts @@ -88,7 +88,7 @@ export default function ({ // we execute the part of expression that fetches the data and store its response const context = await expectExpression('partial_test', expression).getResponse(); - // we reuse that response to render 3 different charts and compare screenshots with baselines + // we reuse that response to render 2 different charts and compare screenshots with baselines const tagCloudExpr = `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`; await ( await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot() @@ -98,11 +98,6 @@ export default function ({ await ( await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot() ).toMatchScreenshot(); - - const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0},"legendPosition":"bottomright","addTooltip":true,"colorSchema":"Yellow to Red","isDisplayWarning":true,"wms":{},"mapZoom":2,"mapCenter":[0,0],"outlineWeight":1,"showAllShapes":true,"selectedLayer":{},"selectedJoinField":{}}'`; - await ( - await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot() - ).toMatchScreenshot(); }); }); }); diff --git a/test/plugin_functional/plugins/core_plugin_a/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_a/server/plugin.ts index 08087356a6cd2..6701ff97cca80 100644 --- a/test/plugin_functional/plugins/core_plugin_a/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_a/server/plugin.ts @@ -22,8 +22,10 @@ export class CorePluginAPlugin implements Plugin { 'pluginA', (context) => { return { - ping: () => - context.core.elasticsearch.legacy.client.callAsInternalUser('ping') as Promise, + ping: async () => { + const { body } = await context.core.elasticsearch.client.asInternalUser.ping(); + return String(body); + }, }; } ); diff --git a/tsconfig.json b/tsconfig.json index 769dd5ecb6a55..0307525de0156 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,4 +19,4 @@ { "path": "./src/plugins/usage_collection/tsconfig.json" }, { "path": "./x-pack/plugins/reporting/tsconfig.json" }, ] -} +} \ No newline at end of file diff --git a/x-pack/examples/alerting_example/kibana.json b/x-pack/examples/alerting_example/kibana.json index f2950db96ba2c..13117713a9a7e 100644 --- a/x-pack/examples/alerting_example/kibana.json +++ b/x-pack/examples/alerting_example/kibana.json @@ -2,9 +2,22 @@ "id": "alertingExample", "version": "0.0.1", "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "server": true, "ui": true, - "requiredPlugins": ["triggersActionsUi", "charts", "data", "alerting", "actions", "kibanaReact", "features", "developerExamples"], + "requiredPlugins": [ + "triggersActionsUi", + "charts", + "data", + "alerting", + "actions", + "kibanaReact", + "features", + "developerExamples" + ], "optionalPlugins": [], "requiredBundles": ["kibanaReact"] } diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx index 58c932c3ca164..55f2b4ccd71e9 100644 --- a/x-pack/examples/embedded_lens_example/public/app.tsx +++ b/x-pack/examples/embedded_lens_example/public/app.tsx @@ -173,7 +173,9 @@ export const App = (props: { timeRange: time, attributes: getLensAttributes(props.defaultIndexPattern!, color), }, - true + { + openInNewTab: true, + } ); // eslint-disable-next-line no-bitwise const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); @@ -195,7 +197,9 @@ export const App = (props: { timeRange: time, attributes: getLensAttributes(props.defaultIndexPattern!, color), }, - false + { + openInNewTab: false, + } ); }} > diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json index 59a0926118962..4fa62668dd557 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json +++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json @@ -5,6 +5,10 @@ "configPath": ["ui_actions_enhanced_examples"], "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": [ "uiActions", "uiActionsEnhanced", @@ -15,10 +19,5 @@ "developerExamples" ], "optionalPlugins": [], - "requiredBundles": [ - "dashboardEnhanced", - "embeddable", - "kibanaUtils", - "kibanaReact" - ] + "requiredBundles": ["dashboardEnhanced", "embeddable", "kibanaUtils", "kibanaReact"] } diff --git a/x-pack/plugins/actions/kibana.json b/x-pack/plugins/actions/kibana.json index ef604a9cf6417..aa3a9f3f6c34c 100644 --- a/x-pack/plugins/actions/kibana.json +++ b/x-pack/plugins/actions/kibana.json @@ -1,5 +1,9 @@ { "id": "actions", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "server": true, "version": "8.0.0", "kibanaVersion": "kibana", diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index 64c759752faec..1274e7b95b114 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server'; +import { + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectsResolveResponse, +} from 'kibana/server'; import { AlertNotifyWhenType } from './alert_notify_when_type'; export type AlertTypeState = Record; @@ -76,6 +80,8 @@ export interface Alert { } export type SanitizedAlert = Omit, 'apiKey'>; +export type ResolvedSanitizedRule = SanitizedAlert & + Omit; export type SanitizedRuleConfig = Pick< SanitizedAlert, diff --git a/x-pack/plugins/alerting/kibana.json b/x-pack/plugins/alerting/kibana.json index af2d08e69f597..82d8de0daf14a 100644 --- a/x-pack/plugins/alerting/kibana.json +++ b/x-pack/plugins/alerting/kibana.json @@ -2,6 +2,10 @@ "id": "alerting", "server": true, "ui": true, + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "alerting"], diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index ad1c97efe2334..c1c7eae45109e 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -22,6 +22,7 @@ import { findRulesRoute } from './find_rules'; import { getRuleAlertSummaryRoute } from './get_rule_alert_summary'; import { getRuleStateRoute } from './get_rule_state'; import { healthRoute } from './health'; +import { resolveRuleRoute } from './resolve_rule'; import { ruleTypesRoute } from './rule_types'; import { muteAllRuleRoute } from './mute_all_rule'; import { muteAlertRoute } from './mute_alert'; @@ -42,6 +43,7 @@ export function defineRoutes(opts: RouteOptions) { defineLegacyRoutes(opts); createRuleRoute(opts); getRuleRoute(router, licenseState); + resolveRuleRoute(router, licenseState); updateRuleRoute(router, licenseState); deleteRuleRoute(router, licenseState); aggregateRulesRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/resolve_rule.test.ts b/x-pack/plugins/alerting/server/routes/resolve_rule.test.ts new file mode 100644 index 0000000000000..b03369a74b865 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/resolve_rule.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pick } from 'lodash'; +import { resolveRuleRoute } from './resolve_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { rulesClientMock } from '../rules_client.mock'; +import { ResolvedSanitizedRule } from '../types'; +import { AsApiContract } from './lib'; + +const rulesClient = rulesClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('resolveRuleRoute', () => { + const mockedRule: ResolvedSanitizedRule<{ + bar: boolean; + }> = { + id: '1', + alertTypeId: '1', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date(), + updatedAt: new Date(), + actions: [ + { + group: 'default', + id: '2', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + consumer: 'bar', + name: 'abc', + tags: ['foo'], + enabled: true, + muteAll: false, + notifyWhen: 'onActionGroupChange', + createdBy: '', + updatedBy: '', + apiKeyOwner: '', + throttle: '30s', + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }; + + const resolveResult: AsApiContract> = { + ...pick( + mockedRule, + 'consumer', + 'name', + 'schedule', + 'tags', + 'params', + 'throttle', + 'enabled', + 'alias_target_id' + ), + rule_type_id: mockedRule.alertTypeId, + notify_when: mockedRule.notifyWhen, + mute_all: mockedRule.muteAll, + created_by: mockedRule.createdBy, + updated_by: mockedRule.updatedBy, + api_key_owner: mockedRule.apiKeyOwner, + muted_alert_ids: mockedRule.mutedInstanceIds, + created_at: mockedRule.createdAt, + updated_at: mockedRule.updatedAt, + id: mockedRule.id, + execution_status: { + status: mockedRule.executionStatus.status, + last_execution_date: mockedRule.executionStatus.lastExecutionDate, + }, + actions: [ + { + group: mockedRule.actions[0].group, + id: mockedRule.actions[0].id, + params: mockedRule.actions[0].params, + connector_type_id: mockedRule.actions[0].actionTypeId, + }, + ], + outcome: 'aliasMatch', + }; + + it('resolves a rule with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + resolveRuleRoute(router, licenseState); + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_resolve"`); + + rulesClient.resolve.mockResolvedValueOnce(mockedRule); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + await handler(context, req, res); + + expect(rulesClient.resolve).toHaveBeenCalledTimes(1); + expect(rulesClient.resolve.mock.calls[0][0].id).toEqual('1'); + + expect(res.ok).toHaveBeenCalledWith({ + body: resolveResult, + }); + }); + + it('ensures the license allows resolving rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + resolveRuleRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + rulesClient.resolve.mockResolvedValueOnce(mockedRule); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents getting rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + resolveRuleRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + rulesClient.resolve.mockResolvedValueOnce(mockedRule); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/resolve_rule.ts b/x-pack/plugins/alerting/server/routes/resolve_rule.ts new file mode 100644 index 0000000000000..011d28780e718 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/resolve_rule.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ILicenseState } from '../lib'; +import { verifyAccessAndContext, RewriteResponseCase } from './lib'; +import { + AlertTypeParams, + AlertingRequestHandlerContext, + INTERNAL_BASE_ALERTING_API_PATH, + ResolvedSanitizedRule, +} from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const rewriteBodyRes: RewriteResponseCase> = ({ + alertTypeId, + createdBy, + updatedBy, + createdAt, + updatedAt, + apiKeyOwner, + notifyWhen, + muteAll, + mutedInstanceIds, + executionStatus, + actions, + scheduledTaskId, + ...rest +}) => ({ + ...rest, + rule_type_id: alertTypeId, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, + api_key_owner: apiKeyOwner, + notify_when: notifyWhen, + mute_all: muteAll, + muted_alert_ids: mutedInstanceIds, + scheduled_task_id: scheduledTaskId, + execution_status: executionStatus && { + ...omit(executionStatus, 'lastExecutionDate'), + last_execution_date: executionStatus.lastExecutionDate, + }, + actions: actions.map(({ group, id, actionTypeId, params }) => ({ + group, + id, + params, + connector_type_id: actionTypeId, + })), +}); + +export const resolveRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_resolve`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = context.alerting.getRulesClient(); + const { id } = req.params; + const rule = await rulesClient.resolve({ id }); + return res.ok({ + body: rewriteBodyRes(rule), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index 4bd197e51a5da..438331a1cd580 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -16,6 +16,7 @@ const createRulesClientMock = () => { aggregate: jest.fn(), create: jest.fn(), get: jest.fn(), + resolve: jest.fn(), getAlertState: jest.fn(), find: jest.fn(), delete: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/audit_events.ts b/x-pack/plugins/alerting/server/rules_client/audit_events.ts index f04b7c3701974..5f6122458ddaf 100644 --- a/x-pack/plugins/alerting/server/rules_client/audit_events.ts +++ b/x-pack/plugins/alerting/server/rules_client/audit_events.ts @@ -11,6 +11,7 @@ import { AuditEvent } from '../../../security/server'; export enum RuleAuditAction { CREATE = 'rule_create', GET = 'rule_get', + RESOLVE = 'rule_resolve', UPDATE = 'rule_update', UPDATE_API_KEY = 'rule_update_api_key', ENABLE = 'rule_enable', @@ -28,6 +29,7 @@ type VerbsTuple = [string, string, string]; const eventVerbs: Record = { rule_create: ['create', 'creating', 'created'], rule_get: ['access', 'accessing', 'accessed'], + rule_resolve: ['access', 'accessing', 'accessed'], rule_update: ['update', 'updating', 'updated'], rule_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], rule_enable: ['enable', 'enabling', 'enabled'], @@ -43,6 +45,7 @@ const eventVerbs: Record = { const eventTypes: Record = { rule_create: 'creation', rule_get: 'access', + rule_resolve: 'access', rule_update: 'change', rule_update_api_key: 'change', rule_enable: 'change', diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index a079a52448e2d..4d191584592a2 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -33,6 +33,7 @@ import { AlertExecutionStatusValues, AlertNotifyWhenType, AlertTypeParams, + ResolvedSanitizedRule, } from '../types'; import { validateAlertTypeParams, @@ -411,6 +412,52 @@ export class RulesClient { ); } + public async resolve({ + id, + }: { + id: string; + }): Promise> { + const { + saved_object: result, + ...resolveResponse + } = await this.unsecuredSavedObjectsClient.resolve('alert', id); + try { + await this.authorization.ensureAuthorized({ + ruleTypeId: result.attributes.alertTypeId, + consumer: result.attributes.consumer, + operation: ReadOperations.Get, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.RESOLVE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.RESOLVE, + savedObject: { type: 'alert', id }, + }) + ); + + const rule = this.getAlertFromRaw( + result.id, + result.attributes.alertTypeId, + result.attributes, + result.references + ); + + return { + ...rule, + ...resolveResponse, + }; + } + public async getAlertState({ id }: { id: string }): Promise { const alert = await this.get({ id }); await this.authorization.ensureAuthorized({ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts new file mode 100644 index 0000000000000..63feb4ff3147a --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts @@ -0,0 +1,451 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RulesClient, ConstructorOptions } from '../rules_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { getBeforeSetup, setGlobalDate } from './lib'; +import { RecoveredActionGroup } from '../../../common'; + +const taskManager = taskManagerMock.createStart(); +const ruleTypeRegistry = ruleTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertingAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); +const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); + +const kibanaVersion = 'v7.10.0'; +const rulesClientParams: jest.Mocked = { + taskManager, + ruleTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertingAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); + (auditLogger.log as jest.Mock).mockClear(); +}); + +setGlobalDate(); + +describe('resolve()', () => { + test('calls saved objects client with given params', async () => { + const rulesClient = new RulesClient(rulesClientParams); + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + notifyWhen: 'onActiveAlert', + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + const result = await rulesClient.resolve({ id: '1' }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alias_target_id": "2", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "notifyWhen": "onActiveAlert", + "outcome": "aliasMatch", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.resolve.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => { + const injectReferencesFn = jest.fn().mockReturnValue({ + bar: true, + parameterThatIsSavedObjectId: '9', + }); + ruleTypeRegistry.get.mockImplementation(() => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: RecoveredActionGroup, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + async executor() {}, + producer: 'alerts', + useSavedObjectReferences: { + extractReferences: jest.fn(), + injectReferences: injectReferencesFn, + }, + })); + const rulesClient = new RulesClient(rulesClientParams); + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + parameterThatIsSavedObjectRef: 'soRef_0', + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + notifyWhen: 'onActiveAlert', + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'param:soRef_0', + type: 'someSavedObjectType', + id: '9', + }, + ], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + const result = await rulesClient.resolve({ id: '1' }); + + expect(injectReferencesFn).toHaveBeenCalledWith( + { + bar: true, + parameterThatIsSavedObjectRef: 'soRef_0', + }, + [{ id: '9', name: 'soRef_0', type: 'someSavedObjectType' }] + ); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alias_target_id": "2", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "notifyWhen": "onActiveAlert", + "outcome": "aliasMatch", + "params": Object { + "bar": true, + "parameterThatIsSavedObjectId": "9", + }, + "schedule": Object { + "interval": "10s", + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + }); + + test(`throws an error when references aren't found`, async () => { + const rulesClient = new RulesClient(rulesClientParams); + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + await expect(rulesClient.resolve({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Action reference \\"action_0\\" not found in alert id: 1"` + ); + }); + + test('throws an error if useSavedObjectReferences.injectReferences throws an error', async () => { + const injectReferencesFn = jest.fn().mockImplementation(() => { + throw new Error('something went wrong!'); + }); + ruleTypeRegistry.get.mockImplementation(() => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: RecoveredActionGroup, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + async executor() {}, + producer: 'alerts', + useSavedObjectReferences: { + extractReferences: jest.fn(), + injectReferences: injectReferencesFn, + }, + })); + const rulesClient = new RulesClient(rulesClientParams); + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + parameterThatIsSavedObjectRef: 'soRef_0', + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + notifyWhen: 'onActiveAlert', + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'soRef_0', + type: 'someSavedObjectType', + id: '9', + }, + ], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + await expect(rulesClient.resolve({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error injecting reference into rule params for rule id 1 - something went wrong!"` + ); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + }); + + test('ensures user is authorised to resolve this type of rule under the consumer', async () => { + const rulesClient = new RulesClient(rulesClientParams); + await rulesClient.resolve({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'rule', + consumer: 'myApp', + operation: 'get', + ruleTypeId: 'myType', + }); + }); + + test('throws when user is not authorised to get this type of alert', async () => { + const rulesClient = new RulesClient(rulesClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "myType" alert for "myApp"`) + ); + + await expect(rulesClient.resolve({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'rule', + consumer: 'myApp', + operation: 'get', + ruleTypeId: 'myType', + }); + }); + }); + + describe('auditLogger', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [], + }, + references: [], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + }); + + test('logs audit event when getting a rule', async () => { + const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger }); + await rulesClient.resolve({ id: '1' }); + expect(auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.objectContaining({ + action: 'rule_resolve', + outcome: 'success', + }), + kibana: { saved_object: { id: '1', type: 'alert' } }, + }) + ); + }); + + test('logs audit event when not authorised to get a rule', async () => { + const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger }); + authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); + + await expect(rulesClient.resolve({ id: '1' })).rejects.toThrow(); + expect(auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.objectContaining({ + action: 'rule_resolve', + outcome: 'failure', + }), + kibana: { + saved_object: { + id: '1', + type: 'alert', + }, + }, + error: { + code: 'Error', + message: 'Unauthorized', + }, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 512f4618792fb..b1460a5fe5cd8 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -1012,6 +1012,410 @@ describe('successful migrations', () => { }); }); }); + + describe('7.15.0', () => { + test('security solution is migrated to saved object references if it has 1 exceptionsList', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', // extra data to ensure we do not overwrite other params + exceptionsList: [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'single', + }, + ], + }, + }); + + expect(migration7150(alert, migrationContext)).toEqual({ + ...alert, + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + ], + }); + }); + + test('security solution is migrated to saved object references if it has 2 exceptionsLists', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', // extra data to ensure we do not overwrite other params + exceptionsList: [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + { + id: '789', + list_id: '0123', + type: 'detection', + namespace_type: 'single', + }, + ], + }, + }); + + expect(migration7150(alert, migrationContext)).toEqual({ + ...alert, + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list-agnostic', + }, + { + name: 'param:exceptionsList_1', + id: '789', + type: 'exception-list', + }, + ], + }); + }); + + test('security solution is migrated to saved object references if it has 3 exceptionsLists', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', // extra data to ensure we do not overwrite other params + exceptionsList: [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'single', + }, + { + id: '789', + list_id: '0123', + type: 'detection', + namespace_type: 'agnostic', + }, + { + id: '101112', + list_id: '777', + type: 'detection', + namespace_type: 'single', + }, + ], + }, + }); + + expect(migration7150(alert, migrationContext)).toEqual({ + ...alert, + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + { + name: 'param:exceptionsList_1', + id: '789', + type: 'exception-list-agnostic', + }, + { + name: 'param:exceptionsList_2', + id: '101112', + type: 'exception-list', + }, + ], + }); + }); + + test('security solution does not change anything if exceptionsList is missing', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + }, + }); + + expect(migration7150(alert, migrationContext)).toEqual(alert); + }); + + test('security solution will keep existing references if we do not have an exceptionsList but we do already have references', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + }, + }), + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + ], + }; + + expect(migration7150(alert, migrationContext)).toEqual({ + ...alert, + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + ], + }); + }); + + test('security solution keep any foreign references if they exist but still migrate other references', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + exceptionsList: [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'single', + }, + { + id: '789', + list_id: '0123', + type: 'detection', + namespace_type: 'single', + }, + { + id: '101112', + list_id: '777', + type: 'detection', + namespace_type: 'single', + }, + ], + }, + }), + references: [ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + ], + }; + + expect(migration7150(alert, migrationContext)).toEqual({ + ...alert, + references: [ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + { + name: 'param:exceptionsList_1', + id: '789', + type: 'exception-list', + }, + { + name: 'param:exceptionsList_2', + id: '101112', + type: 'exception-list', + }, + ], + }); + }); + + test('security solution is idempotent and if re-run on the same migrated data will keep the same items', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + exceptionsList: [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'single', + }, + { + id: '789', + list_id: '0123', + type: 'detection', + namespace_type: 'single', + }, + ], + }, + }), + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + { + name: 'param:exceptionsList_1', + id: '789', + type: 'exception-list', + }, + ], + }; + + expect(migration7150(alert, migrationContext)).toEqual(alert); + }); + + test('security solution will migrate with only missing data if we have partially migrated data', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + exceptionsList: [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'single', + }, + { + id: '789', + list_id: '0123', + type: 'detection', + namespace_type: 'single', + }, + ], + }, + }), + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + ], + }; + + expect(migration7150(alert, migrationContext)).toEqual({ + ...alert, + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + { + name: 'param:exceptionsList_1', + id: '789', + type: 'exception-list', + }, + ], + }); + }); + + test('security solution will not migrate if exception list if it is invalid data', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + exceptionsList: [{ invalid: 'invalid' }], + }, + }), + }; + expect(migration7150(alert, migrationContext)).toEqual(alert); + }); + + test('security solution will migrate valid data if it is mixed with invalid data', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + exceptionsList: [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'single', + }, + { id: 555 }, // <-- Id is a number and not a string, and is invalid + { + id: '456', + list_id: '456', + type: 'detection', + namespace_type: 'single', + }, + ], + }, + }), + }; + expect(migration7150(alert, migrationContext)).toEqual({ + ...alert, + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + { + name: 'param:exceptionsList_1', + id: '456', + type: 'exception-list', + }, + ], + }); + }); + + test('security solution will not migrate if exception list is invalid data but will keep existing references', () => { + const migration7150 = getMigrations(encryptedSavedObjectsSetup)['7.15.0']; + const alert = { + ...getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + exceptionsList: [{ invalid: 'invalid' }], + }, + }), + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + ], + }; + expect(migration7150(alert, migrationContext)).toEqual({ + ...alert, + references: [ + { + name: 'param:exceptionsList_0', + id: '123', + type: 'exception-list', + }, + ], + }); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 944acbdca0182..6823a9b9b20da 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isString } from 'lodash/fp'; import { LogMeta, SavedObjectMigrationMap, @@ -13,6 +14,7 @@ import { SavedObjectMigrationContext, SavedObjectAttributes, SavedObjectAttribute, + SavedObjectReference, } from '../../../../../src/core/server'; import { RawAlert, RawAlertAction } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; @@ -91,12 +93,19 @@ export function getMigrations( pipeMigrations(removeNullAuthorFromSecurityRules) ); + const migrationSecurityRules715 = createEsoMigration( + encryptedSavedObjects, + (doc): doc is SavedObjectUnsanitizedDoc => isSecuritySolutionRule(doc), + pipeMigrations(addExceptionListsToReferences) + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), '7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'), '7.13.0': executeMigrationWithErrorHandling(migrationSecurityRules713, '7.13.0'), '7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'), + '7.15.0': executeMigrationWithErrorHandling(migrationSecurityRules715, '7.15.0'), }; } @@ -467,6 +476,97 @@ function removeNullAuthorFromSecurityRules( }; } +/** + * This migrates exception list containers to saved object references on an upgrade. + * We only migrate if we find these conditions: + * - exceptionLists are an array and not null, undefined, or malformed data. + * - The exceptionList item is an object and id is a string and not null, undefined, or malformed data + * - The existing references do not already have an exceptionItem reference already found within it. + * Some of these issues could crop up during either user manual errors of modifying things, earlier migration + * issues, etc... + * @param doc The document that might have exceptionListItems to migrate + * @returns The document migrated with saved object references + */ +function addExceptionListsToReferences( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { + attributes: { + params: { exceptionsList }, + }, + references, + } = doc; + if (!Array.isArray(exceptionsList)) { + // early return if we are not an array such as being undefined or null or malformed. + return doc; + } else { + const exceptionsToTransform = removeMalformedExceptionsList(exceptionsList); + const newReferences = exceptionsToTransform.flatMap( + (exceptionItem, index) => { + const existingReferenceFound = references?.find((reference) => { + return ( + reference.id === exceptionItem.id && + ((reference.type === 'exception-list' && exceptionItem.namespace_type === 'single') || + (reference.type === 'exception-list-agnostic' && + exceptionItem.namespace_type === 'agnostic')) + ); + }); + if (existingReferenceFound) { + // skip if the reference already exists for some uncommon reason so we do not add an additional one. + // This enables us to be idempotent and you can run this migration multiple times and get the same output. + return []; + } else { + return [ + { + name: `param:exceptionsList_${index}`, + id: String(exceptionItem.id), + type: + exceptionItem.namespace_type === 'agnostic' + ? 'exception-list-agnostic' + : 'exception-list', + }, + ]; + } + } + ); + if (references == null && newReferences.length === 0) { + // Avoid adding an empty references array if the existing saved object never had one to begin with + return doc; + } else { + return { ...doc, references: [...(references ?? []), ...newReferences] }; + } + } +} + +/** + * This will do a flatMap reduce where we only return exceptionsLists and their items if: + * - exceptionLists are an array and not null, undefined, or malformed data. + * - The exceptionList item is an object and id is a string and not null, undefined, or malformed data + * + * Some of these issues could crop up during either user manual errors of modifying things, earlier migration + * issues, etc... + * @param exceptionsList The list of exceptions + * @returns The exception lists if they are a valid enough shape + */ +function removeMalformedExceptionsList( + exceptionsList: SavedObjectAttribute +): SavedObjectAttributes[] { + if (!Array.isArray(exceptionsList)) { + // early return if we are not an array such as being undefined or null or malformed. + return []; + } else { + return exceptionsList.flatMap((exceptionItem) => { + if (!(exceptionItem instanceof Object) || !isString(exceptionItem.id)) { + // return early if we are not an object such as being undefined or null or malformed + // or the exceptionItem.id is not a string from being malformed + return []; + } else { + return [exceptionItem]; + } + }); + } +} + function pipeMigrations(...migrations: AlertMigration[]): AlertMigration { return (doc: SavedObjectUnsanitizedDoc) => migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 650e72751749e..b41ae949d5867 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -86,3 +86,7 @@ export function isIosAgentName(agentName?: string) { const lowercased = agentName && agentName.toLowerCase(); return lowercased === 'ios/swift' || lowercased === 'opentelemetry/swift'; } + +export function isJRubyAgent(agentName?: string, runtimeName?: string) { + return agentName === 'ruby' && runtimeName?.toLowerCase() === 'jruby'; +} diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts index 70c1c7524cfe9..703106628f561 100644 --- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts @@ -27,7 +27,7 @@ export interface SearchServiceParams { start?: string; end?: string; percentileThreshold?: number; - percentileThresholdValue?: number; + analyzeCorrelations?: boolean; } export interface SearchServiceFetchParams extends SearchServiceParams { diff --git a/x-pack/plugins/apm/common/search_strategies/failure_correlations/constants.ts b/x-pack/plugins/apm/common/search_strategies/failure_correlations/constants.ts new file mode 100644 index 0000000000000..a80918f0e399e --- /dev/null +++ b/x-pack/plugins/apm/common/search_strategies/failure_correlations/constants.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY = + 'apmFailedTransactionsCorrelationsSearchStrategy'; + +export const FAILED_TRANSACTIONS_IMPACT_THRESHOLD = { + HIGH: i18n.translate( + 'xpack.apm.correlations.failedTransactions.highImpactText', + { + defaultMessage: 'High', + } + ), + MEDIUM: i18n.translate( + 'xpack.apm.correlations.failedTransactions.mediumImpactText', + { + defaultMessage: 'Medium', + } + ), + LOW: i18n.translate( + 'xpack.apm.correlations.failedTransactions.lowImpactText', + { + defaultMessage: 'Low', + } + ), +} as const; diff --git a/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts new file mode 100644 index 0000000000000..08e05d46ba013 --- /dev/null +++ b/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants'; + +export interface FailedTransactionsCorrelationValue { + key: string; + doc_count: number; + bg_count: number; + score: number; + pValue: number | null; + fieldName: string; + fieldValue: string; +} + +export type FailureCorrelationImpactThreshold = typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD[keyof typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD]; + +export interface CorrelationsTerm { + fieldName: string; + fieldValue: string; +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts index 3f7e01be831f8..3ed6e01ce0c2a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts @@ -18,22 +18,24 @@ describe('APM deep links', () => { cy.contains('APM / Service Map'); // navigates to home page - cy.contains('APM').click(); + // Force click because welcome screen changes + // https://github.com/elastic/kibana/pull/108193 + cy.contains('APM').click({ force: true }); cy.url().should('include', '/apm/services'); cy.get('[data-test-subj="nav-search-input"]').type('APM'); // navigates to services page - cy.contains('APM / Services').click(); + cy.contains('APM / Services').click({ force: true }); cy.url().should('include', '/apm/services'); cy.get('[data-test-subj="nav-search-input"]').type('APM'); // navigates to traces page - cy.contains('APM / Traces').click(); + cy.contains('APM / Traces').click({ force: true }); cy.url().should('include', '/apm/traces'); cy.get('[data-test-subj="nav-search-input"]').type('APM'); // navigates to service maps - cy.contains('APM / Service Map').click(); + cy.contains('APM / Service Map').click({ force: true }); cy.url().should('include', '/apm/service-map'); }); }); diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 5905f700b0bbe..dc52d572e2f35 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -11,12 +11,12 @@ import { stringify } from 'querystring'; import type { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; @@ -36,7 +36,7 @@ const TRANSACTION_TYPE = 'transaction.type'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; const format = ({ pathname, @@ -211,7 +211,7 @@ export function registerApmAlerts( format: ({ fields }) => ({ reason: formatTransactionDurationAnomalyReason({ serviceName: String(fields[SERVICE_NAME][0]), - severityLevel: String(fields[ALERT_SEVERITY_LEVEL]), + severityLevel: String(fields[ALERT_SEVERITY]), measured: Number(fields[ALERT_EVALUATION_VALUE]), }), link: format({ diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx index 5b4ecb8e73752..b1aa4c9231839 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx @@ -15,8 +15,6 @@ import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_ra import { DependenciesTable } from '../../shared/dependencies_table'; import { useApmBackendContext } from '../../../context/apm_backend/use_apm_backend_context'; import { ServiceLink } from '../../shared/service_link'; -import { useApmRouter } from '../../../hooks/use_apm_router'; -import { DependenciesTableServiceMapLink } from '../../shared/dependencies_table/dependencies_table_service_map_link'; export function BackendDetailDependenciesTable() { const { @@ -27,17 +25,6 @@ export function BackendDetailDependenciesTable() { query: { rangeFrom, rangeTo, kuery, environment }, } = useApmParams('/backends/:backendName/overview'); - const router = useApmRouter(); - - const serviceMapLink = router.link('/service-map', { - query: { - rangeFrom, - rangeTo, - environment, - kuery, - }, - }); - const { offset } = getTimeRangeComparison({ start, end, @@ -112,7 +99,6 @@ export function BackendDetailDependenciesTable() { )} status={status} compact={false} - link={} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index 9806d9faaf0b6..1adb41acab70a 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -119,7 +119,7 @@ export function BackendDetailOverview() { - + diff --git a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx index cfee03ef36095..796dae4b84c72 100644 --- a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx @@ -5,19 +5,17 @@ * 2.0. */ +import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { METRIC_TYPE } from '@kbn/analytics'; -import { useApmRouter } from '../../../../hooks/use_apm_router'; +import { useUiTracker } from '../../../../../../observability/public'; import { getNodeName, NodeType } from '../../../../../common/connections'; -import { useApmParams } from '../../../../hooks/use_apm_params'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../../hooks/use_apm_params'; import { useFetcher } from '../../../../hooks/use_fetcher'; -import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; -import { DependenciesTable } from '../../../shared/dependencies_table'; import { BackendLink } from '../../../shared/backend_link'; -import { DependenciesTableServiceMapLink } from '../../../shared/dependencies_table/dependencies_table_service_map_link'; -import { useUiTracker } from '../../../../../../observability/public'; +import { DependenciesTable } from '../../../shared/dependencies_table'; +import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; export function BackendInventoryDependenciesTable() { const { @@ -28,19 +26,8 @@ export function BackendInventoryDependenciesTable() { query: { rangeFrom, rangeTo, environment, kuery }, } = useApmParams('/backends'); - const router = useApmRouter(); - const trackEvent = useUiTracker(); - const serviceMapLink = router.link('/service-map', { - query: { - rangeFrom, - rangeTo, - environment, - kuery, - }, - }); - const { offset } = getTimeRangeComparison({ start, end, @@ -106,12 +93,7 @@ export function BackendInventoryDependenciesTable() { return ( } /> ); } diff --git a/x-pack/plugins/apm/public/components/app/backend_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/backend_inventory/index.tsx index 1d24640f93885..433d187bda0b3 100644 --- a/x-pack/plugins/apm/public/components/app/backend_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_inventory/index.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { EuiSpacer } from '@elastic/eui'; import React from 'react'; import { getKueryBarBoolFilter, @@ -30,6 +31,7 @@ export function BackendInventory() { kueryBarPlaceholder={kueryBarPlaceholder} kueryBarBoolFilter={kueryBarBoolFilter} /> + ); diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx index 62d566963699d..28f671183ed87 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx @@ -7,32 +7,17 @@ import React, { useCallback, useMemo, useState } from 'react'; import { debounce } from 'lodash'; -import { - EuiIcon, - EuiLink, - EuiBasicTable, - EuiBasicTableColumn, - EuiToolTip, -} from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useHistory } from 'react-router-dom'; -import { asInteger, asPercent } from '../../../../common/utils/formatters'; -import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { createHref, push } from '../../shared/Links/url_helpers'; -import { ImpactBar } from '../../shared/ImpactBar'; import { useUiTracker } from '../../../../../observability/public'; import { useTheme } from '../../../hooks/use_theme'; +import { CorrelationsTerm } from '../../../../common/search_strategies/failure_correlations/types'; const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50]; -type CorrelationsApiResponse = - | APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> - | APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'>; -export type SignificantTerm = CorrelationsApiResponse['significantTerms'][0]; - -export type SelectedSignificantTerm = Pick< - SignificantTerm, +export type SelectedCorrelationTerm = Pick< + T, 'fieldName' | 'fieldValue' >; @@ -40,24 +25,22 @@ interface Props { significantTerms?: T[]; status: FETCH_STATUS; percentageColumnName?: string; - setSelectedSignificantTerm: (term: SelectedSignificantTerm | null) => void; + setSelectedSignificantTerm: (term: T | null) => void; selectedTerm?: { fieldName: string; fieldValue: string }; - onFilter: () => void; - columns?: Array>; + onFilter?: () => void; + columns: Array>; } -export function CorrelationsTable({ +export function CorrelationsTable({ significantTerms, status, - percentageColumnName, setSelectedSignificantTerm, - onFilter, columns, selectedTerm, }: Props) { const euiTheme = useTheme(); const trackApmEvent = useUiTracker({ app: 'apm' }); - const trackSelectSignificantTerm = useCallback( + const trackSelectSignificantCorrelationTerm = useCallback( () => debounce( () => trackApmEvent({ metric: 'select_significant_term' }), @@ -65,7 +48,6 @@ export function CorrelationsTable({ ), [trackApmEvent] ); - const history = useHistory(); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); @@ -92,140 +74,6 @@ export function CorrelationsTable({ setPageSize(size); }, []); - const tableColumns: Array> = columns ?? [ - { - width: '116px', - field: 'impact', - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.impactLabel', - { defaultMessage: 'Impact' } - ), - render: (_: any, term: T) => { - return ; - }, - }, - { - field: 'percentage', - name: - percentageColumnName ?? - i18n.translate( - 'xpack.apm.correlations.correlationsTable.percentageLabel', - { defaultMessage: 'Percentage' } - ), - render: (_: any, term: T) => { - return ( - - <>{asPercent(term.valueCount, term.fieldCount)} - - ); - }, - }, - { - field: 'fieldName', - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.fieldNameLabel', - { defaultMessage: 'Field name' } - ), - }, - { - field: 'fieldValue', - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.fieldValueLabel', - { defaultMessage: 'Field value' } - ), - render: (_: any, term: T) => String(term.fieldValue).slice(0, 50), - }, - { - width: '100px', - actions: [ - { - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.filterLabel', - { defaultMessage: 'Filter' } - ), - description: i18n.translate( - 'xpack.apm.correlations.correlationsTable.filterDescription', - { defaultMessage: 'Filter by value' } - ), - icon: 'plusInCircle', - type: 'icon', - onClick: (term: T) => { - push(history, { - query: { - kuery: `${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onFilter(); - trackApmEvent({ metric: 'correlations_term_include_filter' }); - }, - }, - { - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.excludeLabel', - { defaultMessage: 'Exclude' } - ), - description: i18n.translate( - 'xpack.apm.correlations.correlationsTable.excludeDescription', - { defaultMessage: 'Filter out value' } - ), - icon: 'minusInCircle', - type: 'icon', - onClick: (term: T) => { - push(history, { - query: { - kuery: `not ${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onFilter(); - trackApmEvent({ metric: 'correlations_term_exclude_filter' }); - }, - }, - ], - name: i18n.translate( - 'xpack.apm.correlations.correlationsTable.actionsLabel', - { defaultMessage: 'Filter' } - ), - render: (_: any, term: T) => { - return ( - <> - - - -  /  - - - - - ); - }, - }, - ]; - return ( ({ status === FETCH_STATUS.LOADING ? loadingText : noDataText } loading={status === FETCH_STATUS.LOADING} - columns={tableColumns} + columns={columns} rowProps={(term) => { return { onMouseEnter: () => { setSelectedSignificantTerm(term); - trackSelectSignificantTerm(); + trackSelectSignificantCorrelationTerm(); }, onMouseLeave: () => setSelectedSignificantTerm(null), style: diff --git a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx deleted file mode 100644 index 298206f30d614..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Axis, - Chart, - CurveType, - LineSeries, - Position, - ScaleType, - Settings, - timeFormatter, -} from '@elastic/charts'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { useUiTracker } from '../../../../../observability/public'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { useLocalStorage } from '../../../hooks/useLocalStorage'; -import { useApmParams } from '../../../hooks/use_apm_params'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; -import { useTheme } from '../../../hooks/use_theme'; -import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { ChartContainer } from '../../shared/charts/chart_container'; -import { - CorrelationsTable, - SelectedSignificantTerm, -} from './correlations_table'; -import { CustomFields } from './custom_fields'; -import { useFieldNames } from './use_field_names'; - -type OverallErrorsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/errors/overall_timeseries'> ->; - -type CorrelationsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> ->; - -interface Props { - onClose: () => void; -} - -export function ErrorCorrelations({ onClose }: Props) { - const [ - selectedSignificantTerm, - setSelectedSignificantTerm, - ] = useState(null); - - const { serviceName } = useApmServiceContext(); - const { urlParams } = useUrlParams(); - const { transactionName, transactionType, start, end } = urlParams; - const { defaultFieldNames } = useFieldNames(); - const [fieldNames, setFieldNames] = useLocalStorage( - `apm.correlations.errors.fields:${serviceName}`, - defaultFieldNames - ); - const hasFieldNames = fieldNames.length > 0; - - const { - query: { environment, kuery }, - } = useApmParams('/services/:serviceName'); - - const { data: overallData, status: overallStatus } = useFetcher( - (callApmApi) => { - if (start && end) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - }, - }, - }); - } - }, - [ - environment, - kuery, - serviceName, - start, - end, - transactionName, - transactionType, - ] - ); - - const { data: correlationsData, status: correlationsStatus } = useFetcher( - (callApmApi) => { - if (start && end && hasFieldNames) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/errors/failed_transactions', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - fieldNames: fieldNames.join(','), - }, - }, - }); - } - }, - [ - environment, - kuery, - serviceName, - start, - end, - transactionName, - transactionType, - fieldNames, - hasFieldNames, - ] - ); - - const trackApmEvent = useUiTracker({ app: 'apm' }); - trackApmEvent({ metric: 'view_errors_correlations' }); - - return ( - <> - - - -

- {i18n.translate('xpack.apm.correlations.error.description', { - defaultMessage: - 'Why are some transactions failing and returning errors? Correlations will help discover a possible culprit in a particular cohort of your data. Either by host, version, or other custom fields.', - })} -

-
-
- - -

- {i18n.translate('xpack.apm.correlations.error.chart.title', { - defaultMessage: 'Error rate over time', - })} -

-
-
- - - - - - - - - -
- - ); -} - -function getSelectedTimeseries( - significantTerms: CorrelationsApiResponse['significantTerms'], - selectedSignificantTerm: SelectedSignificantTerm -) { - if (!significantTerms) { - return []; - } - return ( - significantTerms.find( - ({ fieldName, fieldValue }) => - selectedSignificantTerm.fieldName === fieldName && - selectedSignificantTerm.fieldValue === fieldValue - )?.timeseries || [] - ); -} - -function ErrorTimeseriesChart({ - overallData, - correlationsData, - selectedSignificantTerm, - status, -}: { - overallData?: OverallErrorsApiResponse; - correlationsData?: CorrelationsApiResponse; - selectedSignificantTerm: SelectedSignificantTerm | null; - status: FETCH_STATUS; -}) { - const theme = useTheme(); - const dateFormatter = timeFormatter('HH:mm:ss'); - - return ( - - - - - - `${roundFloat(d * 100)}%`} - /> - - - - {correlationsData && selectedSignificantTerm ? ( - - ) : null} - - - ); -} - -function roundFloat(n: number, digits = 2) { - const factor = Math.pow(10, digits); - return Math.round(n * factor) / factor; -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx new file mode 100644 index 0000000000000..4fdd908b6faf6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -0,0 +1,443 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useMemo, useState } from 'react'; +import { + EuiCallOut, + EuiCode, + EuiAccordion, + EuiPanel, + EuiBasicTableColumn, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, + EuiText, + EuiBadge, + EuiIcon, + EuiLink, + EuiTitle, + EuiBetaBadge, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory } from 'react-router-dom'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { CorrelationsTable } from './correlations_table'; +import { enableInspectEsQueries } from '../../../../../observability/public'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; +import { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; +import { ImpactBar } from '../../shared/ImpactBar'; +import { isErrorMessage } from './utils/is_error_message'; +import { Summary } from '../../shared/Summary'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { getFailedTransactionsCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label'; +import { createHref, push } from '../../shared/Links/url_helpers'; +import { useUiTracker } from '../../../../../observability/public'; +import { useFailedTransactionsCorrelationsFetcher } from '../../../hooks/use_failed_transactions_correlations_fetcher'; +import { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import { useApmParams } from '../../../hooks/use_apm_params'; + +export function FailedTransactionsCorrelations({ + onFilter, +}: { + onFilter: () => void; +}) { + const { + core: { notifications, uiSettings }, + } = useApmPluginContext(); + const trackApmEvent = useUiTracker({ app: 'apm' }); + + const { serviceName, transactionType } = useApmServiceContext(); + + const { + query: { kuery, environment }, + } = useApmParams('/services/:serviceName'); + + const { urlParams } = useUrlParams(); + const { transactionName, start, end } = urlParams; + + const displayLog = uiSettings.get(enableInspectEsQueries); + + const searchServicePrams: SearchServiceParams = { + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + }; + + const result = useFailedTransactionsCorrelationsFetcher(searchServicePrams); + + const { + ccsWarning, + log, + error, + isRunning, + progress, + startFetch, + cancelFetch, + } = result; + // start fetching on load + // we want this effect to execute exactly once after the component mounts + useEffect(() => { + startFetch(); + + return () => { + // cancel any running async partial request when unmounting the component + // we want this effect to execute exactly once after the component mounts + cancelFetch(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [ + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); + + const selectedTerm = useMemo(() => { + if (selectedSignificantTerm) return selectedSignificantTerm; + return result?.values && + Array.isArray(result.values) && + result.values.length > 0 + ? result?.values[0] + : undefined; + }, [selectedSignificantTerm, result]); + + const history = useHistory(); + + const failedTransactionsCorrelationsColumns: Array< + EuiBasicTableColumn + > = useMemo( + () => [ + { + width: '116px', + field: 'normalizedScore', + name: ( + <> + {i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.pValueLabel', + { + defaultMessage: 'Score', + } + )} + + ), + render: (normalizedScore: number) => { + return ( + <> + + + ); + }, + }, + { + width: '116px', + field: 'pValue', + name: ( + <> + {i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.impactLabel', + { + defaultMessage: 'Impact', + } + )} + + ), + render: getFailedTransactionsCorrelationImpactLabel, + }, + { + field: 'fieldName', + name: i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.fieldNameLabel', + { defaultMessage: 'Field name' } + ), + }, + { + field: 'key', + name: i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.fieldValueLabel', + { defaultMessage: 'Field value' } + ), + render: (fieldValue: string) => String(fieldValue).slice(0, 50), + }, + { + width: '100px', + actions: [ + { + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.filterLabel', + { defaultMessage: 'Filter' } + ), + description: i18n.translate( + 'xpack.apm.correlations.correlationsTable.filterDescription', + { defaultMessage: 'Filter by value' } + ), + icon: 'plusInCircle', + type: 'icon', + onClick: (term: FailedTransactionsCorrelationValue) => { + push(history, { + query: { + kuery: `${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + onFilter(); + trackApmEvent({ metric: 'correlations_term_include_filter' }); + }, + }, + { + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.excludeLabel', + { defaultMessage: 'Exclude' } + ), + description: i18n.translate( + 'xpack.apm.correlations.correlationsTable.excludeDescription', + { defaultMessage: 'Filter out value' } + ), + icon: 'minusInCircle', + type: 'icon', + onClick: (term: FailedTransactionsCorrelationValue) => { + push(history, { + query: { + kuery: `not ${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + onFilter(); + trackApmEvent({ metric: 'correlations_term_exclude_filter' }); + }, + }, + ], + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.actionsLabel', + { defaultMessage: 'Filter' } + ), + render: (_: unknown, term: FailedTransactionsCorrelationValue) => { + return ( + <> + + + +  /  + + + + + ); + }, + }, + ], + [history, onFilter, trackApmEvent] + ); + + useEffect(() => { + if (isErrorMessage(error)) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.correlations.failedTransactions.errorTitle', + { + defaultMessage: + 'An error occurred performing correlations on failed transactions', + } + ), + text: error.toString(), + }); + } + }, [error, notifications.toasts]); + return ( + <> + + + +
+ {i18n.translate( + 'xpack.apm.correlations.failedTransactions.panelTitle', + { + defaultMessage: 'Failed transactions', + } + )} +
+
+
+ + + + +
+ + + + {!isRunning && ( + + + + )} + {isRunning && ( + + + + )} + + + + + + + + + + + + + + + + + + {selectedTerm?.pValue != null ? ( + <> + + + {`${selectedTerm.fieldName}: ${selectedTerm.key}`} + , + <>{`p-value: ${selectedTerm.pValue.toPrecision(3)}`}, + ]} + /> + + + ) : null} + + columns={failedTransactionsCorrelationsColumns} + significantTerms={result?.values} + status={FETCH_STATUS.SUCCESS} + setSelectedSignificantTerm={setSelectedSignificantTerm} + selectedTerm={selectedTerm} + /> + + {ccsWarning && ( + <> + + +

+ {i18n.translate( + 'xpack.apm.correlations.failedTransactions.ccsWarningCalloutBody', + { + defaultMessage: + 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.15 and later versions.', + } + )} +

+
+ + )} + + + {log.length > 0 && displayLog && ( + + + {log.map((d, i) => { + const splitItem = d.split(': '); + return ( +

+ + {splitItem[0]} {splitItem[1]} + +

+ ); + })} +
+
+ )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx new file mode 100644 index 0000000000000..bebc889cc4ed9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { HelpPopover, HelpPopoverButton } from '../help_popover/help_popover'; + +export function FailedTransactionsCorrelationsHelpPopover() { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + { + setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen); + }} + /> + } + closePopover={() => setIsPopoverOpen(false)} + isOpen={isPopoverOpen} + title={i18n.translate('xpack.apm.correlations.failurePopoverTitle', { + defaultMessage: 'Failure correlations', + })} + > +

+ +

+

+ +

+

+ +

+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx deleted file mode 100644 index 57ba75d945ee5..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useState } from 'react'; -import { - EuiButtonEmpty, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, - EuiPortal, - EuiCode, - EuiLink, - EuiCallOut, - EuiButton, - EuiTab, - EuiTabs, - EuiSpacer, - EuiBetaBadge, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useHistory } from 'react-router-dom'; -import { MlLatencyCorrelations } from './ml_latency_correlations'; -import { ErrorCorrelations } from './error_correlations'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { createHref } from '../../shared/Links/url_helpers'; -import { - METRIC_TYPE, - useTrackMetric, -} from '../../../../../observability/public'; -import { isActivePlatinumLicense } from '../../../../common/license_check'; -import { useLicenseContext } from '../../../context/license/use_license_context'; -import { LicensePrompt } from '../../shared/license_prompt'; -import { IUrlParams } from '../../../context/url_params_context/types'; -import { - IStickyProperty, - StickyProperties, -} from '../../shared/sticky_properties'; -import { getEnvironmentLabel } from '../../../../common/environment_filter_values'; -import { - SERVICE_ENVIRONMENT, - SERVICE_NAME, - TRANSACTION_NAME, -} from '../../../../common/elasticsearch_fieldnames'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useApmParams } from '../../../hooks/use_apm_params'; - -const errorRateTab = { - key: 'errorRate', - label: i18n.translate('xpack.apm.correlations.tabs.errorRateLabel', { - defaultMessage: 'Failed transaction rate', - }), - component: ErrorCorrelations, -}; -const latencyCorrelationsTab = { - key: 'latencyCorrelations', - label: i18n.translate('xpack.apm.correlations.tabs.latencyLabel', { - defaultMessage: 'Latency', - }), - component: MlLatencyCorrelations, -}; -const tabs = [latencyCorrelationsTab, errorRateTab]; - -export function Correlations() { - const license = useLicenseContext(); - const hasActivePlatinumLicense = isActivePlatinumLicense(license); - const { urlParams } = useUrlParams(); - const { serviceName } = useApmServiceContext(); - - const { - query: { environment }, - } = useApmParams('/services/:serviceName'); - - const history = useHistory(); - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const [currentTab, setCurrentTab] = useState(latencyCorrelationsTab.key); - const { component: TabContent } = - tabs.find((tab) => tab.key === currentTab) ?? latencyCorrelationsTab; - const metric = { - app: 'apm' as const, - metric: hasActivePlatinumLicense - ? 'correlations_flyout_view' - : 'correlations_license_prompt', - metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT, - }; - useTrackMetric(metric); - useTrackMetric({ ...metric, delay: 15000 }); - - const stickyProperties: IStickyProperty[] = useMemo(() => { - const properties: IStickyProperty[] = []; - if (serviceName !== undefined) { - properties.push({ - label: i18n.translate('xpack.apm.correlations.serviceLabel', { - defaultMessage: 'Service', - }), - fieldName: SERVICE_NAME, - val: serviceName, - width: '20%', - }); - } - - properties.push({ - label: i18n.translate('xpack.apm.correlations.environmentLabel', { - defaultMessage: 'Environment', - }), - fieldName: SERVICE_ENVIRONMENT, - val: getEnvironmentLabel(environment), - width: '20%', - }); - - if (urlParams.transactionName) { - properties.push({ - label: i18n.translate('xpack.apm.correlations.transactionLabel', { - defaultMessage: 'Transaction', - }), - fieldName: TRANSACTION_NAME, - val: urlParams.transactionName, - width: '20%', - }); - } - - return properties; - }, [serviceName, environment, urlParams.transactionName]); - - return ( - <> - { - setIsFlyoutVisible(true); - }} - > - {i18n.translate('xpack.apm.correlations.buttonLabel', { - defaultMessage: 'View correlations', - })} - - - {isFlyoutVisible && ( - - setIsFlyoutVisible(false)} - > - - -

- {CORRELATIONS_TITLE} -   - -

-
- {hasActivePlatinumLicense && ( - <> - - - - {urlParams.kuery ? ( - <> - - - - ) : ( - - )} - - {tabs.map(({ key, label }) => ( - { - setCurrentTab(key); - }} - > - {label} - - ))} - - - )} -
- - {hasActivePlatinumLicense ? ( - <> - setIsFlyoutVisible(false)} /> - - ) : ( - - )} - -
-
- )} - - ); -} - -function Filters({ - urlParams, - history, -}: { - urlParams: IUrlParams; - history: ReturnType; -}) { - if (!urlParams.kuery) { - return null; - } - - return ( - - - {i18n.translate('xpack.apm.correlations.filteringByLabel', { - defaultMessage: 'Filtering by', - })} - - {urlParams.kuery} - - - {i18n.translate('xpack.apm.correlations.clearFiltersLabel', { - defaultMessage: 'Clear', - })} - - - - ); -} - -const CORRELATIONS_TITLE = i18n.translate('xpack.apm.correlations.title', { - defaultMessage: 'Correlations', -}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 0871447337780..6d6e56184e254 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -5,338 +5,462 @@ * 2.0. */ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { - ScaleType, - Chart, - Axis, - BarSeries, - Position, - Settings, -} from '@elastic/charts'; -import React, { useState } from 'react'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + EuiCallOut, + EuiCode, + EuiEmptyPrompt, + EuiAccordion, + EuiPanel, + EuiIcon, + EuiBasicTableColumn, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getDurationFormatter } from '../../../../common/utils/formatters'; +import { FormattedMessage } from '@kbn/i18n/react'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; -import { APIReturnType } from '../../../services/rest/createCallApmApi'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useTransactionLatencyCorrelationsFetcher } from '../../../hooks/use_transaction_latency_correlations_fetcher'; +import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; +import { CorrelationsTable } from './correlations_table'; +import { push } from '../../shared/Links/url_helpers'; import { - CorrelationsTable, - SelectedSignificantTerm, -} from './correlations_table'; -import { ChartContainer } from '../../shared/charts/chart_container'; -import { useTheme } from '../../../hooks/use_theme'; -import { CustomFields, PercentileOption } from './custom_fields'; -import { useFieldNames } from './use_field_names'; -import { useLocalStorage } from '../../../hooks/useLocalStorage'; -import { useUiTracker } from '../../../../../observability/public'; + enableInspectEsQueries, + useUiTracker, +} from '../../../../../observability/public'; +import { asPreciseDecimal } from '../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { LatencyCorrelationsHelpPopover } from './latency_correlations_help_popover'; import { useApmParams } from '../../../hooks/use_apm_params'; +import { isErrorMessage } from './utils/is_error_message'; -type OverallLatencyApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'> ->; +const DEFAULT_PERCENTILE_THRESHOLD = 95; -type CorrelationsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'> ->; - -interface Props { - onClose: () => void; +interface MlCorrelationsTerms { + correlation: number; + ksTest: number; + fieldName: string; + fieldValue: string; + duplicatedFields?: string[]; } -export function LatencyCorrelations({ onClose }: Props) { - const [ - selectedSignificantTerm, - setSelectedSignificantTerm, - ] = useState(null); +export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { + const { + core: { notifications, uiSettings }, + } = useApmPluginContext(); - const { serviceName } = useApmServiceContext(); + const { serviceName, transactionType } = useApmServiceContext(); const { query: { kuery, environment }, } = useApmParams('/services/:serviceName'); const { urlParams } = useUrlParams(); - const { transactionName, transactionType, start, end } = urlParams; - const { defaultFieldNames } = useFieldNames(); - const [fieldNames, setFieldNames] = useLocalStorage( - `apm.correlations.latency.fields:${serviceName}`, - defaultFieldNames - ); - const hasFieldNames = fieldNames.length > 0; - const [ - durationPercentile, - setDurationPercentile, - ] = useLocalStorage( - `apm.correlations.latency.threshold:${serviceName}`, - 75 - ); + const { transactionName, start, end } = urlParams; - const { data: overallData, status: overallStatus } = useFetcher( - (callApmApi) => { - if (start && end) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/latency/overall_distribution', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - }, - }, - }); - } - }, - [ + const displayLog = uiSettings.get(enableInspectEsQueries); + + const { + ccsWarning, + log, + error, + histograms, + percentileThresholdValue, + isRunning, + progress, + startFetch, + cancelFetch, + overallHistogram, + } = useTransactionLatencyCorrelationsFetcher(); + + const startFetchHandler = useCallback(() => { + startFetch({ environment, kuery, serviceName, - start, - end, transactionName, transactionType, - ] - ); + start, + end, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [environment, serviceName, kuery, start, end]); + + // start fetching on load + // we want this effect to execute exactly once after the component mounts + useEffect(() => { + if (isRunning) { + cancelFetch(); + } + + startFetchHandler(); + + return () => { + // cancel any running async partial request when unmounting the component + // we want this effect to execute exactly once after the component mounts + cancelFetch(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [startFetchHandler]); - const maxLatency = overallData?.maxLatency; - const distributionInterval = overallData?.distributionInterval; - const fieldNamesCommaSeparated = fieldNames.join(','); + useEffect(() => { + if (isErrorMessage(error)) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.errorTitle', + { + defaultMessage: 'An error occurred fetching correlations', + } + ), + text: error.toString(), + }); + } + }, [error, notifications.toasts]); - const { data: correlationsData, status: correlationsStatus } = useFetcher( - (callApmApi) => { - if (start && end && hasFieldNames && maxLatency && distributionInterval) { - return callApmApi({ - endpoint: 'GET /api/apm/correlations/latency/slow_transactions', - params: { - query: { - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - durationPercentile: durationPercentile.toString(10), - fieldNames: fieldNamesCommaSeparated, - maxLatency: maxLatency.toString(10), - distributionInterval: distributionInterval.toString(10), + const [ + selectedSignificantTerm, + setSelectedSignificantTerm, + ] = useState(null); + + let selectedHistogram = histograms.length > 0 ? histograms[0] : undefined; + + if (histograms.length > 0 && selectedSignificantTerm !== null) { + selectedHistogram = histograms.find( + (h) => + h.field === selectedSignificantTerm.fieldName && + h.value === selectedSignificantTerm.fieldValue + ); + } + const history = useHistory(); + const trackApmEvent = useUiTracker({ app: 'apm' }); + + const mlCorrelationColumns: Array< + EuiBasicTableColumn + > = useMemo( + () => [ + { + width: '116px', + field: 'correlation', + name: ( + + <> + {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel', + { + defaultMessage: 'Correlation', + } + )} + + + + ), + render: (correlation: number) => { + return
{asPreciseDecimal(correlation, 2)}
; + }, + }, + { + field: 'fieldName', + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel', + { defaultMessage: 'Field name' } + ), + }, + { + field: 'fieldValue', + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldValueLabel', + { defaultMessage: 'Field value' } + ), + render: (fieldValue: string) => String(fieldValue).slice(0, 50), + }, + { + width: '100px', + actions: [ + { + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel', + { defaultMessage: 'Filter' } + ), + description: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription', + { defaultMessage: 'Filter by value' } + ), + icon: 'plusInCircle', + type: 'icon', + onClick: (term: MlCorrelationsTerms) => { + push(history, { + query: { + kuery: `${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + onFilter(); + trackApmEvent({ metric: 'correlations_term_include_filter' }); }, }, - }); - } - }, - [ - environment, - kuery, - serviceName, - start, - end, - transactionName, - transactionType, - durationPercentile, - fieldNamesCommaSeparated, - hasFieldNames, - maxLatency, - distributionInterval, - ] + { + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeLabel', + { defaultMessage: 'Exclude' } + ), + description: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeDescription', + { defaultMessage: 'Filter out value' } + ), + icon: 'minusInCircle', + type: 'icon', + onClick: (term: MlCorrelationsTerms) => { + push(history, { + query: { + kuery: `not ${term.fieldName}:"${encodeURIComponent( + term.fieldValue + )}"`, + }, + }); + onFilter(); + trackApmEvent({ metric: 'correlations_term_exclude_filter' }); + }, + }, + ], + name: i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel', + { defaultMessage: 'Filter' } + ), + }, + ], + [history, onFilter, trackApmEvent] ); - const trackApmEvent = useUiTracker({ app: 'apm' }); - trackApmEvent({ metric: 'view_latency_correlations' }); + const histogramTerms: MlCorrelationsTerms[] = useMemo(() => { + return histograms.map((d) => { + return { + fieldName: d.field, + fieldValue: d.value, + ksTest: d.ksTest, + correlation: d.correlation, + duplicatedFields: d.duplicatedFields, + }; + }); + }, [histograms]); return ( - <> - - - -

- {i18n.translate('xpack.apm.correlations.latency.description', { - defaultMessage: - 'What is slowing down my service? Correlations will help discover a slower performance in a particular cohort of your data. Either by host, version, or other custom fields.', - })} -

-
-
- - - - -

- {i18n.translate( - 'xpack.apm.correlations.latency.chart.title', - { defaultMessage: 'Latency distribution' } - )} -

-
- + + + +
+ {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.panelTitle', + { + defaultMessage: 'Latency distribution', } - status={overallStatus} - selectedSignificantTerm={selectedSignificantTerm} - /> - - + )} +
+
- - - - - + +
- - ); -} -function getAxisMaxes(data?: OverallLatencyApiResponse) { - if (!data?.overallDistribution) { - return { xMax: 0, yMax: 0 }; - } - const { overallDistribution } = data; - const xValues = overallDistribution.map((p) => p.x ?? 0); - const yValues = overallDistribution.map((p) => p.y ?? 0); - return { - xMax: Math.max(...xValues), - yMax: Math.max(...yValues), - }; -} + -function getSelectedDistribution( - significantTerms: CorrelationsApiResponse['significantTerms'], - selectedSignificantTerm: SelectedSignificantTerm -) { - if (!significantTerms) { - return []; - } - return ( - significantTerms.find( - ({ fieldName, fieldValue }) => - selectedSignificantTerm.fieldName === fieldName && - selectedSignificantTerm.fieldValue === fieldValue - )?.distribution || [] - ); -} + -function LatencyDistributionChart({ - overallData, - correlationsData, - selectedSignificantTerm, - status, -}: { - overallData?: OverallLatencyApiResponse; - correlationsData?: CorrelationsApiResponse['significantTerms']; - selectedSignificantTerm: SelectedSignificantTerm | null; - status: FETCH_STATUS; -}) { - const theme = useTheme(); - const { xMax, yMax } = getAxisMaxes(overallData); - const durationFormatter = getDurationFormatter(xMax); + - return ( - - - { - const start = durationFormatter(obj.value); - const end = durationFormatter( - obj.value + overallData?.distributionInterval - ); + + + {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.tableTitle', + { + defaultMessage: 'Correlations', + } + )} + + - return `${start.value} - ${end.formatted}`; - }, - }} - /> - durationFormatter(d).formatted} - /> - `${d}%`} - domain={{ min: 0, max: yMax }} - /> + - + + + + + + + + + + + + + + {!isRunning && ( + + + )} - xScaleType={ScaleType.Linear} - yScaleType={ScaleType.Linear} - xAccessor={'x'} - yAccessors={['y']} - color={theme.eui.euiColorVis1} - data={overallData?.overallDistribution || []} - minBarHeight={5} - tickFormat={(d) => `${roundFloat(d)}%`} - /> - - {correlationsData && selectedSignificantTerm ? ( - + + + )} + +
+ {ccsWarning && ( + <> + + `${roundFloat(d)}%`} + color="warning" + > +

+ {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.ccsWarningCalloutBody', + { + defaultMessage: + 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.14 and later versions.', + } + )} +

+
+ + )} + +
+ {(isRunning || histogramTerms.length > 0) && ( + + columns={mlCorrelationColumns} + significantTerms={histogramTerms} + status={isRunning ? FETCH_STATUS.LOADING : FETCH_STATUS.SUCCESS} + setSelectedSignificantTerm={setSelectedSignificantTerm} + selectedTerm={ + selectedHistogram !== undefined + ? { + fieldName: selectedHistogram.field, + fieldValue: selectedHistogram.value, + } + : undefined + } /> - ) : null} - - + )} + {histogramTerms.length < 1 && (progress === 1 || !isRunning) && ( + <> + + +

+ {i18n.translate( + 'xpack.apm.correlations.latencyCorrelations.noCorrelationsTitle', + { + defaultMessage: 'No significant correlations', + } + )} +

+ + } + body={ + <> + + + + {/* Another EuiText element to enforce a line break */} + + + + + } + /> + + )} +
+ {log.length > 0 && displayLog && ( + + + {log.map((d, i) => { + const splitItem = d.split(': '); + return ( +

+ + {splitItem[0]} {splitItem[1]} + +

+ ); + })} +
+
+ )} + ); } - -function roundFloat(n: number, digits = 2) { - const factor = Math.pow(10, digits); - return Math.round(n * factor) / factor; -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx rename to x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx index 1f9a41c1139cd..64adf0e11fca5 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations_help_popover.tsx @@ -18,6 +18,7 @@ export function LatencyCorrelationsHelpPopover() { anchorPosition="leftUp" button={ { setIsPopoverOpen(!isPopoverOpen); }} diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx deleted file mode 100644 index bbd6648ccaf6e..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useMemo, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import { - EuiCallOut, - EuiCode, - EuiAccordion, - EuiPanel, - EuiIcon, - EuiBasicTableColumn, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiSpacer, - EuiText, - EuiTitle, - EuiToolTip, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { - CorrelationsChart, - replaceHistogramDotsWithBars, -} from './correlations_chart'; -import { - CorrelationsTable, - SelectedSignificantTerm, -} from './correlations_table'; -import { useCorrelations } from './use_correlations'; -import { push } from '../../shared/Links/url_helpers'; -import { - enableInspectEsQueries, - useUiTracker, -} from '../../../../../observability/public'; -import { asPreciseDecimal } from '../../../../common/utils/formatters'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { LatencyCorrelationsHelpPopover } from './ml_latency_correlations_help_popover'; - -const DEFAULT_PERCENTILE_THRESHOLD = 95; -const isErrorMessage = (arg: unknown): arg is Error => { - return arg instanceof Error; -}; - -interface Props { - onClose: () => void; -} - -interface MlCorrelationsTerms { - correlation: number; - ksTest: number; - fieldName: string; - fieldValue: string; - duplicatedFields?: string[]; -} - -export function MlLatencyCorrelations({ onClose }: Props) { - const { - core: { notifications, uiSettings }, - } = useApmPluginContext(); - - const { serviceName, transactionType } = useApmServiceContext(); - const { urlParams } = useUrlParams(); - - const { environment, kuery, transactionName, start, end } = urlParams; - - const displayLog = uiSettings.get(enableInspectEsQueries); - - const { - ccsWarning, - log, - error, - histograms, - percentileThresholdValue, - isRunning, - progress, - startFetch, - cancelFetch, - overallHistogram: originalOverallHistogram, - } = useCorrelations({ - ...{ - ...{ - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - }, - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - }, - }); - - const overallHistogram = useMemo( - () => replaceHistogramDotsWithBars(originalOverallHistogram), - [originalOverallHistogram] - ); - - // start fetching on load - // we want this effect to execute exactly once after the component mounts - useEffect(() => { - startFetch(); - - return () => { - // cancel any running async partial request when unmounting the component - // we want this effect to execute exactly once after the component mounts - cancelFetch(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (isErrorMessage(error)) { - notifications.toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.errorTitle', - { - defaultMessage: 'An error occurred fetching correlations', - } - ), - text: error.toString(), - }); - } - }, [error, notifications.toasts]); - - const [ - selectedSignificantTerm, - setSelectedSignificantTerm, - ] = useState(null); - - let selectedHistogram = histograms.length > 0 ? histograms[0] : undefined; - - if (histograms.length > 0 && selectedSignificantTerm !== null) { - selectedHistogram = histograms.find( - (h) => - h.field === selectedSignificantTerm.fieldName && - h.value === selectedSignificantTerm.fieldValue - ); - } - const history = useHistory(); - const trackApmEvent = useUiTracker({ app: 'apm' }); - - const mlCorrelationColumns: Array< - EuiBasicTableColumn - > = useMemo( - () => [ - { - width: '116px', - field: 'correlation', - name: ( - - <> - {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel', - { - defaultMessage: 'Correlation', - } - )} - - - - ), - render: (correlation: number) => { - return
{asPreciseDecimal(correlation, 2)}
; - }, - }, - { - field: 'fieldName', - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel', - { defaultMessage: 'Field name' } - ), - }, - { - field: 'fieldValue', - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldValueLabel', - { defaultMessage: 'Field value' } - ), - render: (fieldValue: string) => String(fieldValue).slice(0, 50), - }, - { - width: '100px', - actions: [ - { - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel', - { defaultMessage: 'Filter' } - ), - description: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription', - { defaultMessage: 'Filter by value' } - ), - icon: 'plusInCircle', - type: 'icon', - onClick: (term: MlCorrelationsTerms) => { - push(history, { - query: { - kuery: `${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onClose(); - trackApmEvent({ metric: 'correlations_term_include_filter' }); - }, - }, - { - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeLabel', - { defaultMessage: 'Exclude' } - ), - description: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.excludeDescription', - { defaultMessage: 'Filter out value' } - ), - icon: 'minusInCircle', - type: 'icon', - onClick: (term: MlCorrelationsTerms) => { - push(history, { - query: { - kuery: `not ${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, - }, - }); - onClose(); - trackApmEvent({ metric: 'correlations_term_exclude_filter' }); - }, - }, - ], - name: i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel', - { defaultMessage: 'Filter' } - ), - }, - ], - [history, onClose, trackApmEvent] - ); - - const histogramTerms: MlCorrelationsTerms[] = useMemo(() => { - return histograms.map((d) => { - return { - fieldName: d.field, - fieldValue: d.value, - ksTest: d.ksTest, - correlation: d.correlation, - duplicatedFields: d.duplicatedFields, - }; - }); - }, [histograms]); - - return ( - <> - - - {!isRunning && ( - - - - )} - {isRunning && ( - - - - )} - - - - - - - - - - - - - - - - - - - {ccsWarning && ( - <> - - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.ccsWarningCalloutBody', - { - defaultMessage: - 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.14 and later versions.', - } - )} -

-
- - )} - - - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.chartTitle', - { - defaultMessage: 'Latency distribution for {name} (Log-Log Plot)', - values: { - name: transactionName ?? serviceName, - }, - } - )} -

-
- - - - - -
- {histograms.length > 0 && selectedHistogram !== undefined && ( - - )} - {histograms.length < 1 && progress > 0.99 ? ( - <> - - - - - - ) : null} -
- {log.length > 0 && displayLog && ( - - - {log.map((d, i) => { - const splitItem = d.split(': '); - return ( -

- - {splitItem[0]} {splitItem[1]} - -

- ); - })} -
-
- )} - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts deleted file mode 100644 index 05cb367a9fde7..0000000000000 --- a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useRef, useState } from 'react'; -import type { Subscription } from 'rxjs'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - isCompleteResponse, - isErrorResponse, -} from '../../../../../../../src/plugins/data/public'; -import type { - HistogramItem, - SearchServiceValue, -} from '../../../../common/search_strategies/correlations/types'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApmPluginStartDeps } from '../../../plugin'; - -interface CorrelationsOptions { - environment?: string; - kuery?: string; - serviceName?: string; - transactionName?: string; - transactionType?: string; - start?: string; - end?: string; -} - -interface RawResponse { - percentileThresholdValue?: number; - took: number; - values: SearchServiceValue[]; - overallHistogram: HistogramItem[]; - log: string[]; - ccsWarning: boolean; -} - -export const useCorrelations = (params: CorrelationsOptions) => { - const { - services: { data }, - } = useKibana(); - - const [error, setError] = useState(); - const [isComplete, setIsComplete] = useState(false); - const [isRunning, setIsRunning] = useState(false); - const [loaded, setLoaded] = useState(0); - const [rawResponse, setRawResponse] = useState(); - const [timeTook, setTimeTook] = useState(); - const [total, setTotal] = useState(100); - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(); - - function setResponse(response: IKibanaSearchResponse) { - // @TODO: optimize rawResponse.overallHistogram if histogram is the same - setIsRunning(response.isRunning || false); - setRawResponse(response.rawResponse); - setLoaded(response.loaded!); - setTotal(response.total!); - setTimeTook(response.rawResponse.took); - } - - const startFetch = () => { - setError(undefined); - setIsComplete(false); - searchSubscription$.current?.unsubscribe(); - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - - const req = { params }; - - // Submit the search request using the `data.search` service. - searchSubscription$.current = data.search - .search>(req, { - strategy: 'apmCorrelationsSearchStrategy', - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (res: IKibanaSearchResponse) => { - setResponse(res); - if (isCompleteResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setIsRunning(false); - setIsComplete(true); - } else if (isErrorResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setError((res as unknown) as Error); - setIsRunning(false); - } - }, - error: (e: Error) => { - setError(e); - setIsRunning(false); - }, - }); - }; - - const cancelFetch = () => { - searchSubscription$.current?.unsubscribe(); - searchSubscription$.current = undefined; - abortCtrl.current.abort(); - setIsRunning(false); - }; - - return { - ccsWarning: rawResponse?.ccsWarning ?? false, - log: rawResponse?.log ?? [], - error, - histograms: rawResponse?.values ?? [], - percentileThresholdValue: - rawResponse?.percentileThresholdValue ?? undefined, - overallHistogram: rawResponse?.overallHistogram, - isComplete, - isRunning, - progress: loaded / total, - timeTook, - startFetch, - cancelFetch, - }; -}; diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts new file mode 100644 index 0000000000000..d133ed1060ebe --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failure_correlations/constants'; + +describe('getFailedTransactionsCorrelationImpactLabel', () => { + it('returns null if value is invalid ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(-0.03)).toBe(null); + expect(getFailedTransactionsCorrelationImpactLabel(NaN)).toBe(null); + expect(getFailedTransactionsCorrelationImpactLabel(Infinity)).toBe(null); + }); + + it('returns null if value is greater than or equal to the threshold ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(0.02)).toBe(null); + expect(getFailedTransactionsCorrelationImpactLabel(0.1)).toBe(null); + }); + + it('returns High if value is within [0, 1e-6) ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(0)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH + ); + expect(getFailedTransactionsCorrelationImpactLabel(1e-7)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH + ); + }); + + it('returns Medium if value is within [1e-6, 1e-3) ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(1e-6)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM + ); + expect(getFailedTransactionsCorrelationImpactLabel(1e-5)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM + ); + expect(getFailedTransactionsCorrelationImpactLabel(1e-4)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM + ); + }); + + it('returns Low if value is within [1e-3, 0.02) ', () => { + expect(getFailedTransactionsCorrelationImpactLabel(1e-3)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW + ); + expect(getFailedTransactionsCorrelationImpactLabel(0.009)).toBe( + FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW + ); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts new file mode 100644 index 0000000000000..af64c50617019 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FailureCorrelationImpactThreshold } from '../../../../../common/search_strategies/failure_correlations/types'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failure_correlations/constants'; + +export function getFailedTransactionsCorrelationImpactLabel( + pValue: number +): FailureCorrelationImpactThreshold | null { + // The lower the p value, the higher the impact + if (pValue >= 0 && pValue < 1e-6) + return FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH; + if (pValue >= 1e-6 && pValue < 0.001) + return FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM; + if (pValue >= 0.001 && pValue < 0.02) + return FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW; + + return null; +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/is_error_message.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/is_error_message.ts new file mode 100644 index 0000000000000..06eb75d6b3314 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/is_error_message.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const isErrorMessage = (arg: unknown): arg is Error => { + return arg instanceof Error; +}; diff --git a/x-pack/plugins/apm/public/components/app/help_popover/help_popover.tsx b/x-pack/plugins/apm/public/components/app/help_popover/help_popover.tsx index def310f1d8140..930cece64f96a 100644 --- a/x-pack/plugins/apm/public/components/app/help_popover/help_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/help_popover/help_popover.tsx @@ -8,6 +8,7 @@ import React, { ReactNode } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiButtonEmpty, EuiButtonIcon, EuiLinkButtonProps, EuiPopover, @@ -23,18 +24,36 @@ const PopoverContent = euiStyled(EuiText)` `; export function HelpPopoverButton({ + buttonTextEnabled = false, onClick, }: { + buttonTextEnabled?: boolean; onClick: EuiLinkButtonProps['onClick']; }) { + const buttonText = i18n.translate('xpack.apm.helpPopover.ariaLabel', { + defaultMessage: 'Help', + }); + + if (buttonTextEnabled) { + return ( + + {buttonText} + + ); + } + return ( ); diff --git a/x-pack/plugins/apm/public/components/app/service_dependencies/index.tsx b/x-pack/plugins/apm/public/components/app/service_dependencies/index.tsx index 1488c4773359c..bffe8997684a2 100644 --- a/x-pack/plugins/apm/public/components/app/service_dependencies/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dependencies/index.tsx @@ -31,7 +31,7 @@ export function ServiceDependencies() { - + ); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index a5774a6fdbe95..a3ad01b4442ed 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -16,8 +16,6 @@ import { useUrlParams } from '../../../context/url_params_context/use_url_params import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useApmParams } from '../../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; -import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; -import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; import { SearchBar } from '../../shared/search_bar'; import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison'; @@ -159,9 +157,6 @@ export function ServiceInventory() { query: { environment, kuery }, } = useApmParams('/services'); - const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ - kuery, - }); const { mainStatisticsData, mainStatisticsStatus, @@ -197,11 +192,6 @@ export function ServiceInventory() { setUserHasDismissedCallout(true)} />
)} - {fallbackToTransactions && ( - - - - )} ; type Items = ServiceListAPIResponse['items']; @@ -161,7 +163,7 @@ export function getServiceColumns({ /> ), align: 'left', - width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', + width: showWhenSmallOrGreaterThanLarge ? `${unit * 11}px` : 'auto', }, { field: 'throughput', @@ -182,7 +184,7 @@ export function getServiceColumns({ /> ), align: 'left', - width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', + width: showWhenSmallOrGreaterThanLarge ? `${unit * 11}px` : 'auto', }, { field: 'transactionErrorRate', @@ -237,6 +239,11 @@ export function ServiceList({ const { query } = useApmParams('/services'); + const { kuery } = query; + const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ + kuery, + }); + const serviceColumns = useMemo( () => getServiceColumns({ @@ -256,14 +263,18 @@ export function ServiceList({ : 'transactionsPerMinute'; return ( - + - + + {fallbackToTransactions && ( + + + + )} { beforeAll(() => { mockMoment(); + + const callApmApiSpy = getCallApmApiSpy().mockImplementation( + ({ endpoint }) => { + if (endpoint === 'GET /api/apm/fallback_to_transactions') { + return Promise.resolve({ fallbackToTransactions: false }); + } + return Promise.reject(`Response for ${endpoint} is not defined`); + } + ); + + getCreateCallApmApiSpy().mockImplementation(() => callApmApiSpy as any); }); it('renders empty state', () => { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index a9fdd11805840..601aba269112c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; import { isRumAgentName, isIosAgentName } from '../../../../common/agent_name'; import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context'; @@ -22,7 +23,8 @@ import { ServiceOverviewThroughputChart } from './service_overview_throughput_ch import { TransactionsTable } from '../../shared/transactions_table'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; -import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; +import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; +import { useApmRouter } from '../../../hooks/use_apm_router'; /** * The height a chart should be if it's next to a table with 5 rows and a title. @@ -33,6 +35,7 @@ export const chartHeight = 288; export function ServiceOverview() { const { agentName, serviceName } = useApmServiceContext(); const { + query, query: { environment, kuery }, } = useApmParams('/services/:serviceName/overview'); const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ @@ -46,6 +49,14 @@ export function ServiceOverview() { const isRumAgent = isRumAgentName(agentName); const isIosAgent = isIosAgentName(agentName); + const router = useApmRouter(); + const dependenciesLink = router.link('/services/:serviceName/dependencies', { + path: { + serviceName, + }, + query, + }); + return ( {fallbackToTransactions && ( - + )} @@ -82,7 +93,11 @@ export function ServiceOverview() { - + @@ -126,7 +141,17 @@ export function ServiceOverview() { {!isRumAgent && ( - + + {i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableTabLink', + { defaultMessage: 'View dependencies' } + )} + + } + /> )} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index 642725727eedf..a589ffebd8ecc 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import { EuiLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; import { METRIC_TYPE } from '@kbn/analytics'; +import { i18n } from '@kbn/i18n'; +import React, { ReactNode } from 'react'; import { useUiTracker } from '../../../../../../observability/public'; -import { useApmRouter } from '../../../../hooks/use_apm_router'; import { getNodeName, NodeType } from '../../../../../common/connections'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; @@ -21,7 +19,15 @@ import { DependenciesTable } from '../../../shared/dependencies_table'; import { ServiceLink } from '../../../shared/service_link'; import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; -export function ServiceOverviewDependenciesTable() { +interface ServiceOverviewDependenciesTableProps { + fixedHeight?: boolean; + link?: ReactNode; +} + +export function ServiceOverviewDependenciesTable({ + fixedHeight, + link, +}: ServiceOverviewDependenciesTableProps) { const { urlParams: { start, @@ -33,7 +39,6 @@ export function ServiceOverviewDependenciesTable() { } = useUrlParams(); const { - query, query: { environment, kuery, rangeFrom, rangeTo }, } = useApmParams('/services/:serviceName/*'); @@ -46,15 +51,6 @@ export function ServiceOverviewDependenciesTable() { const { serviceName, transactionType } = useApmServiceContext(); - const router = useApmRouter(); - - const dependenciesLink = router.link('/services/:serviceName/dependencies', { - path: { - serviceName, - }, - query, - }); - const trackEvent = useUiTracker(); const { data, status } = useFetcher( @@ -78,7 +74,7 @@ export function ServiceOverviewDependenciesTable() { data?.serviceDependencies.map((dependency) => { const { location } = dependency; const name = getNodeName(location); - const link = + const itemLink = location.type === NodeType.backend ? ( - {i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableTabLink', - { defaultMessage: 'View dependencies' } - )} - - } + link={link} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index 68e6873caf2f8..dc10bf413bfe9 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -203,6 +203,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { ; const DEFAULT_RESPONSE: TracesAPIResponse = { @@ -58,7 +58,7 @@ export function TraceOverview() { {fallbackToTransactions && ( - + )} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx deleted file mode 100644 index ba007015b25f8..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/custom_tooltip.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { TooltipInfo } from '@elastic/charts'; -import { EuiIcon, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { TimeFormatter } from '../../../../../common/utils/formatters'; -import { useTheme } from '../../../../hooks/use_theme'; -import { formatYLong, IChartPoint } from './'; - -export function CustomTooltip( - props: TooltipInfo & { - serie?: IChartPoint; - isSamplesEmpty: boolean; - timeFormatter: TimeFormatter; - } -) { - const theme = useTheme(); - const { values, header, serie, isSamplesEmpty, timeFormatter } = props; - const { color, value } = values[0]; - - let headerTitle = `${timeFormatter(header?.value)}`; - if (serie) { - const xFormatted = timeFormatter(serie.x); - const x0Formatted = timeFormatter(serie.x0); - headerTitle = `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; - } - - return ( -
- <> -
{headerTitle}
-
-
-
-
-
-
- {formatYLong(value)} - {value} -
-
-
- - {isSamplesEmpty && ( -
- - - {i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSamplesAvailable', - { defaultMessage: 'No samples available' } - )} - -
- )} -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts deleted file mode 100644 index 5d6d73f36fac1..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getFormattedBuckets } from './index'; - -describe('Distribution', () => { - it('getFormattedBuckets', () => { - const buckets = [ - { key: 0, count: 0, samples: [] }, - { key: 20, count: 0, samples: [] }, - { key: 40, count: 0, samples: [] }, - { - key: 60, - count: 5, - samples: [ - { - transactionId: 'someTransactionId', - traceId: 'someTraceId', - }, - ], - }, - { - key: 80, - count: 100, - samples: [ - { - transactionId: 'anotherTransactionId', - traceId: 'anotherTraceId', - }, - ], - }, - ]; - - expect(getFormattedBuckets(buckets, 20)).toEqual([ - { x: 20, x0: 0, y: 0, style: { cursor: 'default' } }, - { x: 40, x0: 20, y: 0, style: { cursor: 'default' } }, - { x: 60, x0: 40, y: 0, style: { cursor: 'default' } }, - { - x: 80, - x0: 60, - y: 5, - style: { cursor: 'pointer' }, - }, - { - x: 100, - x0: 80, - y: 100, - style: { cursor: 'pointer' }, - }, - ]); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx deleted file mode 100644 index 4ff094c025451..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Axis, - Chart, - HistogramBarSeries, - Position, - ProjectionClickListener, - RectAnnotation, - ScaleType, - Settings, - SettingsSpec, - TooltipInfo, - XYChartSeriesIdentifier, -} from '@elastic/charts'; -import { EuiIconTip, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import d3 from 'd3'; -import { isEmpty, keyBy } from 'lodash'; -import React from 'react'; -import { ValuesType } from 'utility-types'; -import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { useTheme } from '../../../../hooks/use_theme'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { unit } from '../../../../utils/style'; -import { ChartContainer } from '../../../shared/charts/chart_container'; -import { EmptyMessage } from '../../../shared/EmptyMessage'; -import { CustomTooltip } from './custom_tooltip'; - -type TransactionDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; - -type DistributionBucket = TransactionDistributionAPIResponse['buckets'][0]; - -export interface IChartPoint { - x0: number; - x: number; - y: number; - style: { - cursor: string; - }; -} - -export function getFormattedBuckets( - buckets?: DistributionBucket[], - bucketSize?: number -) { - if (!buckets || !bucketSize) { - return []; - } - - return buckets.map( - ({ samples, count, key }): IChartPoint => { - return { - x0: key, - x: key + bucketSize, - y: count, - style: { - cursor: isEmpty(samples) ? 'default' : 'pointer', - }, - }; - } - ); -} - -const formatYShort = (t: number) => { - return i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel', - { - defaultMessage: '{transCount} trans.', - values: { transCount: t }, - } - ); -}; - -export const formatYLong = (t: number) => { - return i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel', - { - defaultMessage: - '{transCount, plural, =0 {transactions} one {transaction} other {transactions}}', - values: { - transCount: t, - }, - } - ); -}; - -interface Props { - distribution?: TransactionDistributionAPIResponse; - fetchStatus: FETCH_STATUS; - bucketIndex: number; - onBucketClick: ( - bucket: ValuesType - ) => void; -} - -export function TransactionDistribution({ - distribution, - fetchStatus, - bucketIndex, - onBucketClick, -}: Props) { - const theme = useTheme(); - - // no data in response - if ( - (!distribution || distribution.noHits) && - fetchStatus !== FETCH_STATUS.LOADING - ) { - return ( - - ); - } - - const buckets = getFormattedBuckets( - distribution?.buckets, - distribution?.bucketSize - ); - - const xMin = d3.min(buckets, (d) => d.x0) || 0; - const xMax = d3.max(buckets, (d) => d.x0) || 0; - const timeFormatter = getDurationFormatter(xMax); - - const distributionMap = keyBy(distribution?.buckets, 'key'); - const bucketsMap = keyBy(buckets, 'x0'); - - const tooltip: SettingsSpec['tooltip'] = { - stickTo: 'top', - customTooltip: (props: TooltipInfo) => { - const datum = props.header?.datum as IChartPoint; - const selectedDistribution = distributionMap[datum?.x0]; - const serie = bucketsMap[datum?.x0]; - return ( - - ); - }, - }; - - const onBarClick: ProjectionClickListener = ({ x }) => { - const clickedBucket = distribution?.buckets.find((bucket) => { - return bucket.key === x; - }); - if (clickedBucket) { - onBucketClick(clickedBucket); - } - }; - - const selectedBucket = buckets[bucketIndex]; - - return ( -
- -
- {i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle', - { - defaultMessage: 'Latency distribution', - } - )}{' '} - -
-
- - - - {selectedBucket && ( - - )} - timeFormatter(time).formatted} - /> - formatYShort(value)} - /> - value} - minBarHeight={2} - id="transactionDurationDistribution" - name={(series: XYChartSeriesIdentifier) => { - const bucketCount = series.splitAccessors.get( - series.yAccessor - ) as number; - return formatYLong(bucketCount); - }} - splitSeriesAccessors={['y']} - xScaleType={ScaleType.Linear} - yScaleType={ScaleType.Linear} - xAccessor="x0" - yAccessors={['y']} - data={buckets} - color={theme.eui.euiColorVis1} - /> - - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx new file mode 100644 index 0000000000000..49d28fec1a136 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { BrushEndListener, XYBrushArea } from '@elastic/charts'; +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useTransactionDistributionFetcher } from '../../../../hooks/use_transaction_distribution_fetcher'; +import { TransactionDistributionChart } from '../../../shared/charts/transaction_distribution_chart'; +import { useUiTracker } from '../../../../../../observability/public'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { useApmParams } from '../../../../hooks/use_apm_params'; +import { isErrorMessage } from '../../correlations/utils/is_error_message'; + +const DEFAULT_PERCENTILE_THRESHOLD = 95; + +interface Props { + markerCurrentTransaction?: number; + onChartSelection: BrushEndListener; + onClearSelection: () => void; + selection?: [number, number]; +} + +export function TransactionDistribution({ + markerCurrentTransaction, + onChartSelection, + onClearSelection, + selection, +}: Props) { + const { + core: { notifications }, + } = useApmPluginContext(); + + const { serviceName, transactionType } = useApmServiceContext(); + + const { + query: { kuery, environment }, + } = useApmParams('/services/:serviceName'); + + const { urlParams } = useUrlParams(); + + const { transactionName, start, end } = urlParams; + + const emptySelectionText = i18n.translate( + 'xpack.apm.transactionDetails.emptySelectionText', + { + defaultMessage: 'Click and drag to select a range', + } + ); + + const clearSelectionAriaLabel = i18n.translate( + 'xpack.apm.transactionDetails.clearSelectionAriaLabel', + { + defaultMessage: 'Clear selection', + } + ); + + const { + error, + percentileThresholdValue, + isRunning, + startFetch, + cancelFetch, + transactionDistribution, + } = useTransactionDistributionFetcher(); + + useEffect(() => { + if (isRunning) { + cancelFetch(); + } + + startFetch({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }); + + return () => { + // cancel any running async partial request when unmounting the component + // we want this effect to execute exactly once after the component mounts + cancelFetch(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [environment, serviceName, kuery, start, end]); + + useEffect(() => { + if (isErrorMessage(error)) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.transactionDetails.distribution.errorTitle', + { + defaultMessage: 'An error occurred fetching the distribution', + } + ), + text: error.toString(), + }); + } + }, [error, notifications.toasts]); + + const trackApmEvent = useUiTracker({ app: 'apm' }); + + const onTrackedChartSelection: BrushEndListener = ( + brushArea: XYBrushArea + ) => { + onChartSelection(brushArea); + trackApmEvent({ metric: 'transaction_distribution_chart_selection' }); + }; + + const onTrackedClearSelection = () => { + onClearSelection(); + trackApmEvent({ metric: 'transaction_distribution_chart_clear_selection' }); + }; + + return ( +
+ + + +
+ {i18n.translate( + 'xpack.apm.transactionDetails.distribution.panelTitle', + { + defaultMessage: 'Latency distribution', + } + )} +
+
+
+ {!selection && ( + + + + + + + {emptySelectionText} + + + + )} + {selection && ( + + + {i18n.translate( + 'xpack.apm.transactionDetails.distribution.selectionText', + { + defaultMessage: `Selection: {selectionFrom} - {selectionTo}ms`, + values: { + selectionFrom: Math.round(selection[0] / 1000), + selectionTo: Math.round(selection[1] / 1000), + }, + } + )} + + + )} +
+ + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx new file mode 100644 index 0000000000000..8743b8f3ea811 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + METRIC_TYPE, + useTrackMetric, +} from '../../../../../observability/public'; + +import { isActivePlatinumLicense } from '../../../../common/license_check'; + +import { useLicenseContext } from '../../../context/license/use_license_context'; + +import { LicensePrompt } from '../../shared/license_prompt'; + +import { FailedTransactionsCorrelations } from '../correlations/failed_transactions_correlations'; + +import type { TabContentProps } from './types'; + +function FailedTransactionsCorrelationsTab({ onFilter }: TabContentProps) { + const license = useLicenseContext(); + + const hasActivePlatinumLicense = isActivePlatinumLicense(license); + + const metric = { + app: 'apm' as const, + metric: hasActivePlatinumLicense + ? 'failed_transactions_tab_view' + : 'failed_transactions_license_prompt', + metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT, + }; + useTrackMetric(metric); + useTrackMetric({ ...metric, delay: 15000 }); + + return hasActivePlatinumLicense ? ( + + ) : ( + + ); +} + +export const failedTransactionsCorrelationsTab = { + dataTestSubj: 'apmFailedTransactionsCorrelationsTabButton', + key: 'failedTransactionsCorrelations', + label: ( + <> + {i18n.translate( + 'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel', + { + defaultMessage: 'Failed transaction correlations', + } + )} + + ), + component: FailedTransactionsCorrelationsTab, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 143e82649facd..0c6f03047dc7d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -5,55 +5,23 @@ * 2.0. */ -import { EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { flatten, isEmpty } from 'lodash'; +import { EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; -import { useHistory } from 'react-router-dom'; import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; -import { HeightRetainer } from '../../shared/HeightRetainer'; -import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; -import { TransactionDistribution } from './Distribution'; -import { useWaterfallFetcher } from './use_waterfall_fetcher'; -import { WaterfallWithSummary } from './waterfall_with_summary'; -interface Sample { - traceId: string; - transactionId: string; -} +import { TransactionDetailsTabs } from './transaction_details_tabs'; export function TransactionDetails() { - const { urlParams } = useUrlParams(); - const history = useHistory(); - - const { - waterfall, - exceedsMax, - status: waterfallStatus, - } = useWaterfallFetcher(); - const { path, query } = useApmParams( '/services/:serviceName/transactions/view' ); - - const apmRouter = useApmRouter(); - const { transactionName } = query; - const { - distributionData, - distributionStatus, - } = useTransactionDistributionFetcher({ - transactionName, - environment: query.environment, - kuery: query.kuery, - }); + const apmRouter = useApmRouter(); useBreadcrumb({ title: transactionName, @@ -63,36 +31,6 @@ export function TransactionDetails() { }), }); - const selectedSample = flatten( - distributionData.buckets.map((bucket) => bucket.samples) - ).find( - (sample) => - sample.transactionId === urlParams.transactionId && - sample.traceId === urlParams.traceId - ); - - const bucketWithSample = - selectedSample && - distributionData.buckets.find((bucket) => - bucket.samples.includes(selectedSample) - ); - - const traceSamples = bucketWithSample?.samples ?? []; - const bucketIndex = bucketWithSample - ? distributionData.buckets.indexOf(bucketWithSample) - : -1; - - const selectSampleFromBucketClick = (sample: Sample) => { - history.push({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - transactionId: sample.transactionId, - traceId: sample.traceId, - }), - }); - }; - return ( <> @@ -110,32 +48,9 @@ export function TransactionDetails() { /> - - - - { - if (!isEmpty(bucket.samples)) { - selectSampleFromBucketClick(bucket.samples[0]); - } - }} - /> - - - + - - - + ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx new file mode 100644 index 0000000000000..df3e75a3c8f2e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/latency_correlations_tab.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + METRIC_TYPE, + useTrackMetric, +} from '../../../../../observability/public'; + +import { isActivePlatinumLicense } from '../../../../common/license_check'; + +import { useLicenseContext } from '../../../context/license/use_license_context'; + +import { LicensePrompt } from '../../shared/license_prompt'; + +import { LatencyCorrelations } from '../correlations/latency_correlations'; + +import type { TabContentProps } from './types'; + +function LatencyCorrelationsTab({ onFilter }: TabContentProps) { + const license = useLicenseContext(); + + const hasActivePlatinumLicense = isActivePlatinumLicense(license); + + const metric = { + app: 'apm' as const, + metric: hasActivePlatinumLicense + ? 'correlations_tab_view' + : 'correlations_license_prompt', + metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT, + }; + useTrackMetric(metric); + useTrackMetric({ ...metric, delay: 15000 }); + + return hasActivePlatinumLicense ? ( + + ) : ( + + ); +} + +export const latencyCorrelationsTab = { + dataTestSubj: 'apmLatencyCorrelationsTabButton', + key: 'latencyCorrelations', + label: i18n.translate('xpack.apm.transactionDetails.tabs.latencyLabel', { + defaultMessage: 'Latency correlations', + }), + component: LatencyCorrelationsTab, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx new file mode 100644 index 0000000000000..0421fcd055d6c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; + +import { TransactionDistribution } from './distribution'; +import { useWaterfallFetcher } from './use_waterfall_fetcher'; +import type { TabContentProps } from './types'; +import { WaterfallWithSummary } from './waterfall_with_summary'; + +function TraceSamplesTab({ + selectSampleFromChartSelection, + clearChartSelection, + sampleRangeFrom, + sampleRangeTo, + traceSamples, +}: TabContentProps) { + const { urlParams } = useUrlParams(); + + const { + waterfall, + exceedsMax, + status: waterfallStatus, + } = useWaterfallFetcher(); + + return ( + <> + + + + + + + ); +} + +export const traceSamplesTab = { + dataTestSubj: 'apmTraceSamplesTabButton', + key: 'traceSamples', + label: i18n.translate('xpack.apm.transactionDetails.tabs.traceSamplesLabel', { + defaultMessage: 'Trace samples', + }), + component: TraceSamplesTab, +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx new file mode 100644 index 0000000000000..160d41bfa9bde --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useState } from 'react'; + +import { omit } from 'lodash'; +import { useHistory } from 'react-router-dom'; + +import { XYBrushArea } from '@elastic/charts'; +import { EuiPanel, EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; + +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useTransactionTraceSamplesFetcher } from '../../../hooks/use_transaction_trace_samples_fetcher'; + +import { maybe } from '../../../../common/utils/maybe'; +import { HeightRetainer } from '../../shared/HeightRetainer'; +import { fromQuery, push, toQuery } from '../../shared/Links/url_helpers'; + +import { failedTransactionsCorrelationsTab } from './failed_transactions_correlations_tab'; +import { latencyCorrelationsTab } from './latency_correlations_tab'; +import { traceSamplesTab } from './trace_samples_tab'; + +const tabs = [ + traceSamplesTab, + latencyCorrelationsTab, + failedTransactionsCorrelationsTab, +]; + +export function TransactionDetailsTabs() { + const { query } = useApmParams('/services/:serviceName/transactions/view'); + + const { urlParams } = useUrlParams(); + const history = useHistory(); + + const [currentTab, setCurrentTab] = useState(traceSamplesTab.key); + const { component: TabContent } = + tabs.find((tab) => tab.key === currentTab) ?? traceSamplesTab; + + const { environment, kuery, transactionName } = query; + const { traceSamplesData } = useTransactionTraceSamplesFetcher({ + transactionName, + kuery, + environment, + }); + + const selectSampleFromChartSelection = (selection: XYBrushArea) => { + if (selection !== undefined) { + const { x } = selection; + if (Array.isArray(x)) { + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + sampleRangeFrom: Math.round(x[0]), + sampleRangeTo: Math.round(x[1]), + }), + }); + } + } + }; + + const { sampleRangeFrom, sampleRangeTo, transactionId, traceId } = urlParams; + const { traceSamples } = traceSamplesData; + + const clearChartSelection = () => { + // enforces a reset of the current sample to be highlighted in the chart + // and selected in waterfall section below, otherwise we end up with + // stale data for the selected sample + push(history, { + query: { + sampleRangeFrom: '', + sampleRangeTo: '', + traceId: '', + transactionId: '', + }, + }); + }; + + // When filtering in either the latency correlations or failed transactions correlations tab, + // scroll to the top of the page and switch to the 'Trace samples' tab to trigger a refresh. + const traceSamplesTabKey = traceSamplesTab.key; + const onFilter = useCallback(() => { + // Scroll to the top of the page + window.scrollTo(0, 0); + // Switch back to the 'trace samples' tab + setCurrentTab(traceSamplesTabKey); + }, [traceSamplesTabKey]); + + useEffect(() => { + const selectedSample = traceSamples.find( + (sample) => + sample.transactionId === transactionId && sample.traceId === traceId + ); + + if (!selectedSample) { + // selected sample was not found. select a new one: + const preferredSample = maybe(traceSamples[0]); + + history.replace({ + ...history.location, + search: fromQuery({ + ...omit(toQuery(history.location.search), [ + 'traceId', + 'transactionId', + ]), + ...preferredSample, + }), + }); + } + }, [history, traceSamples, transactionId, traceId]); + + return ( + <> + + {tabs.map(({ dataTestSubj, key, label }) => ( + { + setCurrentTab(key); + }} + > + {label} + + ))} + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/types.ts b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts new file mode 100644 index 0000000000000..1ccb3d01a9b28 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { XYBrushArea } from '@elastic/charts'; + +import type { TraceSample } from '../../../hooks/use_transaction_trace_samples_fetcher'; + +export interface TabContentProps { + clearChartSelection: () => void; + onFilter: () => void; + sampleRangeFrom?: number; + sampleRangeTo?: number; + selectSampleFromChartSelection: (selection: XYBrushArea) => void; + traceSamples: TraceSample[]; +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx index 64c4e7dcb42b9..19199cda9495e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx @@ -10,34 +10,29 @@ import { EuiFlexGroup, EuiFlexItem, EuiPagination, - EuiPanel, EuiSpacer, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import type { IUrlParams } from '../../../../context/url_params_context/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; import { TransactionActionMenu } from '../../../shared/transaction_action_menu/TransactionActionMenu'; +import type { TraceSample } from '../../../../hooks/use_transaction_trace_samples_fetcher'; import { MaybeViewTraceLink } from './MaybeViewTraceLink'; import { TransactionTabs } from './TransactionTabs'; import { IWaterfall } from './waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; import { useApmParams } from '../../../../hooks/use_apm_params'; -type DistributionApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; - -type DistributionBucket = DistributionApiResponse['buckets'][0]; - interface Props { urlParams: IUrlParams; waterfall: IWaterfall; exceedsMax: boolean; isLoading: boolean; - traceSamples: DistributionBucket['samples']; + traceSamples: TraceSample[]; } export function WaterfallWithSummary({ @@ -88,13 +83,13 @@ export function WaterfallWithSummary({ /> ); - return {content}; + return content; } const entryTransaction = entryWaterfallTransaction.doc; return ( - + <> @@ -142,6 +137,6 @@ export function WaterfallWithSummary({ waterfall={waterfall} exceedsMax={exceedsMax} /> - + ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index e53ca324eac0a..39e317569a0ae 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -14,10 +14,11 @@ import { IUrlParams } from '../../../context/url_params_context/types'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; -import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; +import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { TransactionsTable } from '../../shared/transactions_table'; + import { useRedirect } from './useRedirect'; function getRedirectLocation({ @@ -69,7 +70,7 @@ export function TransactionOverview() { <> - + diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index c7dd0f46cfc22..8d7d14191a851 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -16,7 +16,7 @@ import { UrlParamsProvider } from '../../../context/url_params_context/url_param import { IUrlParams } from '../../../context/url_params_context/types'; import * as useFetcherHook from '../../../hooks/use_fetcher'; import * as useServiceTransactionTypesHook from '../../../context/apm_service/use_service_transaction_types_fetcher'; -import * as useServiceAgentNameHook from '../../../context/apm_service/use_service_agent_name_fetcher'; +import * as useServiceAgentNameHook from '../../../context/apm_service/use_service_agent_fetcher'; import { disableConsoleWarning, renderWithTheme, @@ -52,9 +52,10 @@ function setup({ // mock agent jest - .spyOn(useServiceAgentNameHook, 'useServiceAgentNameFetcher') + .spyOn(useServiceAgentNameHook, 'useServiceAgentFetcher') .mockReturnValue({ agentName: 'nodejs', + runtimeName: 'node', error: undefined, status: useFetcherHook.FETCH_STATUS.SUCCESS, }); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx new file mode 100644 index 0000000000000..5a481b2d6f10c --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { isMetricsTabHidden, isJVMsTabHidden } from './'; + +describe('APM service template', () => { + describe('isMetricsTabHidden', () => { + describe('hides metrics tab', () => { + [ + { agentName: undefined }, + { agentName: 'js-base' }, + { agentName: 'rum-js' }, + { agentName: 'opentelemetry/webjs' }, + { agentName: 'java' }, + { agentName: 'opentelemetry/java' }, + { agentName: 'ios/swift' }, + { agentName: 'opentelemetry/swift' }, + { agentName: 'ruby', runtimeName: 'jruby' }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isMetricsTabHidden(input)).toBeTruthy(); + }); + }); + }); + describe('shows metrics tab', () => { + [ + { agentName: 'ruby', runtimeName: 'ruby' }, + { agentName: 'ruby' }, + { agentName: 'dotnet' }, + { agentName: 'go' }, + { agentName: 'nodejs' }, + { agentName: 'php' }, + { agentName: 'python' }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isMetricsTabHidden(input)).toBeFalsy(); + }); + }); + }); + }); + describe('isJVMsTabHidden', () => { + describe('hides JVMs tab', () => { + [ + { agentName: undefined }, + { agentName: 'ruby', runtimeName: 'ruby' }, + { agentName: 'ruby' }, + { agentName: 'dotnet' }, + { agentName: 'go' }, + { agentName: 'nodejs' }, + { agentName: 'php' }, + { agentName: 'python' }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isJVMsTabHidden(input)).toBeTruthy(); + }); + }); + }); + describe('shows JVMs tab', () => { + [ + { agentName: 'java' }, + { agentName: 'opentelemetry/java' }, + { agentName: 'ruby', runtimeName: 'jruby' }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isJVMsTabHidden(input)).toBeFalsy(); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index d332048338cc0..c12fdab09613c 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { isIosAgentName, isJavaAgentName, + isJRubyAgent, isRumAgentName, } from '../../../../../common/agent_name'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; @@ -26,7 +27,6 @@ import { useApmServiceContext } from '../../../../context/apm_service/use_apm_se import { useBreadcrumb } from '../../../../context/breadcrumbs/use_breadcrumb'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { useApmRouter } from '../../../../hooks/use_apm_router'; -import { Correlations } from '../../../app/correlations'; import { SearchBar } from '../../../shared/search_bar'; import { ServiceIcons } from '../../../shared/service_icons'; import { ApmMainTemplate } from '../apm_main_template'; @@ -108,10 +108,6 @@ function TemplateWithContext({ - - - - ), }} @@ -123,8 +119,34 @@ function TemplateWithContext({ ); } +export function isMetricsTabHidden({ + agentName, + runtimeName, +}: { + agentName?: string; + runtimeName?: string; +}) { + return ( + !agentName || + isRumAgentName(agentName) || + isJavaAgentName(agentName) || + isIosAgentName(agentName) || + isJRubyAgent(agentName, runtimeName) + ); +} + +export function isJVMsTabHidden({ + agentName, + runtimeName, +}: { + agentName?: string; + runtimeName?: string; +}) { + return !(isJavaAgentName(agentName) || isJRubyAgent(agentName, runtimeName)); +} + function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { - const { agentName } = useApmServiceContext(); + const { agentName, runtimeName } = useApmServiceContext(); const { config } = useApmPluginContext(); const router = useApmRouter(); @@ -172,6 +194,8 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { label: i18n.translate('xpack.apm.serviceDetails.dependenciesTabLabel', { defaultMessage: 'Dependencies', }), + hidden: + !agentName || isRumAgentName(agentName) || isIosAgentName(agentName), }, { key: 'errors', @@ -192,11 +216,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', { defaultMessage: 'Metrics', }), - hidden: - !agentName || - isRumAgentName(agentName) || - isJavaAgentName(agentName) || - isIosAgentName(agentName), + hidden: isMetricsTabHidden({ agentName, runtimeName }), }, { key: 'nodes', @@ -207,7 +227,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { label: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', { defaultMessage: 'JVMs', }), - hidden: !isJavaAgentName(agentName), + hidden: isJVMsTabHidden({ agentName, runtimeName }), }, { key: 'service-map', diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index 9acc04f18f187..b0cadd50b3d61 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -65,6 +65,8 @@ export function createHref( } export type APMQueryParams = { + sampleRangeFrom?: number; + sampleRangeTo?: number; transactionId?: string; transactionName?: string; transactionType?: string; diff --git a/x-pack/plugins/apm/public/components/shared/aggregated_transactions_badge/index.tsx b/x-pack/plugins/apm/public/components/shared/aggregated_transactions_badge/index.tsx new file mode 100644 index 0000000000000..69cc78f1e72c2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/aggregated_transactions_badge/index.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function AggregatedTransactionsBadge() { + return ( +
+ + + {i18n.translate('xpack.apm.aggregatedTransactions.fallback.badge', { + defaultMessage: `Based on sampled transactions`, + })} + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx b/x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx deleted file mode 100644 index 71aeb54d43702..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiCallOut, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export function AggregatedTransactionsCallout() { - return ( - - {i18n.translate('xpack.apm.aggregatedTransactions.callout.title', { - defaultMessage: `This page is using transaction event data as no metrics events were found in the current time range.`, - })} - - } - iconType="iInCircle" - /> - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx index 0f09b042a587b..25a37570182bf 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx @@ -13,7 +13,7 @@ import { ALERT_ID, ALERT_RULE_PRODUCER, ALERT_RULE_CONSUMER, - ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY, ALERT_START, ALERT_STATUS, ALERT_UUID, @@ -163,7 +163,7 @@ describe('getAlertAnnotations', () => { describe('with an alert with a warning severity', () => { const warningAlert: Alert = { ...alert, - [ALERT_SEVERITY_LEVEL]: ['warning'], + [ALERT_SEVERITY]: ['warning'], }; it('uses the warning color', () => { @@ -196,7 +196,7 @@ describe('getAlertAnnotations', () => { describe('with an alert with a critical severity', () => { const criticalAlert: Alert = { ...alert, - [ALERT_SEVERITY_LEVEL]: ['critical'], + [ALERT_SEVERITY]: ['critical'], }; it('uses the critical color', () => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx index f51494b8fa1d8..4aef5f6e56b96 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx @@ -14,7 +14,7 @@ import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { ALERT_DURATION as ALERT_DURATION_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_TYPED, ALERT_START as ALERT_START_TYPED, ALERT_UUID as ALERT_UUID_TYPED, ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, @@ -22,7 +22,7 @@ import type { } from '@kbn/rule-data-utils'; import { ALERT_DURATION as ALERT_DURATION_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, ALERT_START as ALERT_START_NON_TYPED, ALERT_UUID as ALERT_UUID_NON_TYPED, ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, @@ -38,7 +38,7 @@ import { asDuration, asPercent } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; const ALERT_UUID: typeof ALERT_UUID_TYPED = ALERT_UUID_NON_TYPED; const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; @@ -119,7 +119,7 @@ export function getAlertAnnotations({ new Date(parsed[ALERT_START]!).getTime() ); const end = start + parsed[ALERT_DURATION]! / 1000; - const severityLevel = parsed[ALERT_SEVERITY_LEVEL]; + const severityLevel = parsed[ALERT_SEVERITY]; const color = getAlertColor({ severityLevel, theme }); const header = getAlertHeader({ severityLevel }); const formatter = getFormatter(parsed[ALERT_RULE_TYPE_ID]!); diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx index f9b22c422e3e3..17fdef952658d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx @@ -11,7 +11,7 @@ import { ALERT_RULE_TYPE_ID, ALERT_EVALUATION_VALUE, ALERT_ID, - ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY, ALERT_START, ALERT_STATUS, ALERT_UUID, @@ -158,7 +158,7 @@ Example.args = { tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], [ALERT_RULE_PRODUCER]: ['apm'], - [ALERT_SEVERITY_LEVEL]: ['warning'], + [ALERT_SEVERITY]: ['warning'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478181'], [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], @@ -180,7 +180,7 @@ Example.args = { tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], [ALERT_RULE_PRODUCER]: ['apm'], - [ALERT_SEVERITY_LEVEL]: ['critical'], + [ALERT_SEVERITY]: ['critical'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478182'], [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx similarity index 52% rename from x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx rename to x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index e3ff631ae1a6f..3e8a8cc260a56 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -5,99 +5,66 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { AnnotationDomainType, + AreaSeries, + Axis, + BrushEndListener, Chart, CurveType, - Settings, - Axis, - ScaleType, - Position, - AreaSeries, - RecursivePartial, - AxisStyle, - PartialTheme, LineAnnotation, LineAnnotationDatum, + Position, + RectAnnotation, + ScaleType, + Settings, + LineAnnotationStyle, } from '@elastic/charts'; -import euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useChartTheme } from '../../../../../../observability/public'; + import { getDurationUnitKey, getUnitLabelAndConvertedValue, -} from '../../../../common/utils/formatters'; - -import { HistogramItem } from '../../../../common/search_strategies/correlations/types'; - -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useTheme } from '../../../hooks/use_theme'; +} from '../../../../../common/utils/formatters'; +import { HistogramItem } from '../../../../../common/search_strategies/correlations/types'; -import { ChartContainer } from '../../shared/charts/chart_container'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useTheme } from '../../../../hooks/use_theme'; -const { euiColorMediumShade } = euiVars; -const axisColor = euiColorMediumShade; - -const axes: RecursivePartial = { - axisLine: { - stroke: axisColor, - }, - tickLabel: { - fontSize: 10, - fill: axisColor, - padding: 0, - }, - tickLine: { - stroke: axisColor, - size: 5, - }, - gridLine: { - horizontal: { - dash: [1, 2], - }, - vertical: { - strokeWidth: 1, - }, - }, -}; -const chartTheme: PartialTheme = { - axes, - legend: { - spacingBuffer: 100, - }, - areaSeriesStyle: { - line: { - visible: false, - }, - }, -}; +import { ChartContainer } from '../chart_container'; interface CorrelationsChartProps { field?: string; value?: string; histogram?: HistogramItem[]; + markerCurrentTransaction?: number; markerValue: number; markerPercentile: number; overallHistogram?: HistogramItem[]; + onChartSelection?: BrushEndListener; + selection?: [number, number]; } -const annotationsStyle = { +const getAnnotationsStyle = (color = 'gray'): LineAnnotationStyle => ({ line: { strokeWidth: 1, - stroke: 'gray', + stroke: color, opacity: 0.8, }, details: { fontSize: 8, fontFamily: 'Arial', fontStyle: 'normal', - fill: 'gray', + fill: color, padding: 0, }, -}; +}); const CHART_PLACEHOLDER_VALUE = 0.0001; @@ -123,21 +90,30 @@ export const replaceHistogramDotsWithBars = ( } }; -export function CorrelationsChart({ +export function TransactionDistributionChart({ field, value, histogram: originalHistogram, + markerCurrentTransaction, markerValue, markerPercentile, overallHistogram, + onChartSelection, + selection, }: CorrelationsChartProps) { + const chartTheme = useChartTheme(); const euiTheme = useTheme(); + const patchedOverallHistogram = useMemo( + () => replaceHistogramDotsWithBars(overallHistogram), + [overallHistogram] + ); + const annotationsDataValues: LineAnnotationDatum[] = [ { dataValue: markerValue, details: i18n.translate( - 'xpack.apm.correlations.latency.chart.percentileMarkerLabel', + 'xpack.apm.transactionDistribution.chart.percentileMarkerLabel', { defaultMessage: '{markerPercentile}th percentile', values: { @@ -159,6 +135,21 @@ export function CorrelationsChart({ const histogram = replaceHistogramDotsWithBars(originalHistogram); + const selectionAnnotation = + selection !== undefined + ? [ + { + coordinates: { + x0: selection[0], + x1: selection[1], + y0: 0, + y1: 100000, + }, + details: 'selection', + }, + ] + : undefined; + return (
0} + hasData={ + Array.isArray(patchedOverallHistogram) && + patchedOverallHistogram.length > 0 + } status={ - Array.isArray(overallHistogram) + Array.isArray(patchedOverallHistogram) ? FETCH_STATUS.SUCCESS : FETCH_STATUS.LOADING } @@ -176,15 +170,75 @@ export function CorrelationsChart({ + {selectionAnnotation !== undefined && ( + + )} + {typeof markerCurrentTransaction === 'number' && ( + + )} @@ -203,25 +257,27 @@ export function CorrelationsChart({ : converted.convertedValue; return `${convertedValue}${converted.unitLabel}`; }} + gridLine={{ visible: false }} /> {title} - {link} + {link && {link}} ` - ${({ shouldUseMobileLayout }) => - shouldUseMobileLayout + ${({ fixedHeight, shouldUseMobileLayout }) => + shouldUseMobileLayout || !fixedHeight ? '' : ` min-height: ${tableHeight}px; @@ -54,15 +55,18 @@ const OverviewTableContainerDiv = euiStyled.div<{ export function OverviewTableContainer({ children, + fixedHeight, isEmptyAndLoading, }: { children?: ReactNode; + fixedHeight?: boolean; isEmptyAndLoading: boolean; }) { const { isMedium } = useBreakPoints(); return ( diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 6a736f8009a74..2f7b1b01021e3 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -60,10 +60,12 @@ interface Props { numberOfTransactionsPerPage?: number; showAggregationAccurateCallout?: boolean; environment: string; + fixedHeight?: boolean; kuery: string; } export function TransactionsTable({ + fixedHeight = false, hideViewTransactionsLink = false, numberOfTransactionsPerPage = 5, showAggregationAccurateCallout = false, @@ -301,6 +303,7 @@ export function TransactionsTable({ ({ serviceName: '', transactionTypes: [], alerts: [] }); export function ApmServiceContextProvider({ @@ -40,7 +41,7 @@ export function ApmServiceContextProvider({ query, } = useApmParams('/services/:serviceName'); - const { agentName } = useServiceAgentNameFetcher(serviceName); + const { agentName, runtimeName } = useServiceAgentFetcher(serviceName); const transactionTypes = useServiceTransactionTypesFetcher(serviceName); @@ -65,6 +66,7 @@ export function ApmServiceContextProvider({ transactionType, transactionTypes, alerts, + runtimeName, }} children={children} /> diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts similarity index 70% rename from x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts rename to x-pack/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts index 82198eb73b3cb..214b72a34d6e5 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts @@ -8,14 +8,19 @@ import { useFetcher } from '../../hooks/use_fetcher'; import { useUrlParams } from '../url_params_context/use_url_params'; -export function useServiceAgentNameFetcher(serviceName?: string) { +const INITIAL_STATE = { + agentName: undefined, + runtimeName: undefined, +}; + +export function useServiceAgentFetcher(serviceName?: string) { const { urlParams } = useUrlParams(); const { start, end } = urlParams; - const { data, error, status } = useFetcher( + const { data = INITIAL_STATE, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end) { return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/agent_name', + endpoint: 'GET /api/apm/services/{serviceName}/agent', params: { path: { serviceName }, query: { start, end }, @@ -26,5 +31,5 @@ export function useServiceAgentNameFetcher(serviceName?: string) { [serviceName, start, end] ); - return { agentName: data?.agentName, status, error }; + return { ...data, status, error }; } diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index 37e8dc82a0408..c1b56a4979765 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -30,6 +30,8 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { const query = toQuery(location.search); const { + sampleRangeFrom, + sampleRangeTo, traceId, transactionId, transactionName, @@ -73,6 +75,8 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { pageSize: pageSize ? toNumber(pageSize) : undefined, transactionId: toString(transactionId), traceId: toString(traceId), + sampleRangeFrom: sampleRangeFrom ? toNumber(sampleRangeFrom) : undefined, + sampleRangeTo: sampleRangeTo ? toNumber(sampleRangeTo) : undefined, waterfallItemId: toString(waterfallItemId), detailTab: toString(detailTab), flyoutDetailTab: toString(flyoutDetailTab), diff --git a/x-pack/plugins/apm/public/context/url_params_context/types.ts b/x-pack/plugins/apm/public/context/url_params_context/types.ts index 370cbfec156b1..68b672362a1da 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/types.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/types.ts @@ -24,6 +24,8 @@ export type IUrlParams = { sortDirection?: string; sortField?: string; start?: string; + sampleRangeFrom?: number; + sampleRangeTo?: number; traceId?: string; transactionId?: string; transactionName?: string; diff --git a/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts new file mode 100644 index 0000000000000..3841419e860fc --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef, useState } from 'react'; +import type { Subscription } from 'rxjs'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + isCompleteResponse, + isErrorResponse, +} from '../../../../../src/plugins/data/public'; +import type { SearchServiceParams } from '../../common/search_strategies/correlations/types'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../plugin'; +import { FailedTransactionsCorrelationValue } from '../../common/search_strategies/failure_correlations/types'; +import { FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY } from '../../common/search_strategies/failure_correlations/constants'; + +interface RawResponse { + took: number; + values: FailedTransactionsCorrelationValue[]; + log: string[]; + ccsWarning: boolean; +} + +interface FailedTransactionsCorrelationsFetcherState { + error?: Error; + isComplete: boolean; + isRunning: boolean; + loaded: number; + ccsWarning: RawResponse['ccsWarning']; + values: RawResponse['values']; + log: RawResponse['log']; + timeTook?: number; + total: number; +} + +export const useFailedTransactionsCorrelationsFetcher = ( + params: Omit +) => { + const { + services: { data }, + } = useKibana(); + + const [ + fetchState, + setFetchState, + ] = useState({ + isComplete: false, + isRunning: false, + loaded: 0, + ccsWarning: false, + values: [], + log: [], + total: 100, + }); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + + function setResponse(response: IKibanaSearchResponse) { + setFetchState((prevState) => ({ + ...prevState, + isRunning: response.isRunning || false, + ccsWarning: response.rawResponse?.ccsWarning ?? false, + values: response.rawResponse?.values ?? [], + log: response.rawResponse?.log ?? [], + loaded: response.loaded!, + total: response.total!, + timeTook: response.rawResponse.took, + })); + } + + const startFetch = () => { + setFetchState((prevState) => ({ + ...prevState, + error: undefined, + isComplete: false, + })); + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + const req = { params }; + + // Submit the search request using the `data.search` service. + searchSubscription$.current = data.search + .search>(req, { + strategy: FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY, + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (res: IKibanaSearchResponse) => { + setResponse(res); + if (isCompleteResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + isRunnning: false, + isComplete: true, + })); + } else if (isErrorResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + error: (res as unknown) as Error, + setIsRunning: false, + })); + } + }, + error: (error: Error) => { + setFetchState((prevState) => ({ + ...prevState, + error, + setIsRunning: false, + })); + }, + }); + }; + + const cancelFetch = () => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + setFetchState((prevState) => ({ + ...prevState, + setIsRunning: false, + })); + }; + + return { + ...fetchState, + progress: fetchState.loaded / fetchState.total, + startFetch, + cancelFetch, + }; +}; diff --git a/x-pack/plugins/apm/public/hooks/use_time_range.test.ts b/x-pack/plugins/apm/public/hooks/use_time_range.test.ts index dbdd7de171650..9ce104939d4ff 100644 --- a/x-pack/plugins/apm/public/hooks/use_time_range.test.ts +++ b/x-pack/plugins/apm/public/hooks/use_time_range.test.ts @@ -4,11 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - act, - renderHook, - RenderHookResult, -} from '@testing-library/react-hooks'; +import { renderHook, RenderHookResult } from '@testing-library/react-hooks'; import { useTimeRange } from './use_time_range'; describe('useTimeRange', () => { @@ -29,40 +25,33 @@ describe('useTimeRange', () => { ); }); - afterEach(() => {}); - it('returns the parsed range on first render', () => { expect(hook.result.current.start).toEqual('2021-01-01T11:45:00.000Z'); expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); }); it('only changes the parsed range when rangeFrom/rangeTo change', () => { - Date.now = jest.fn(() => new Date(Date.UTC(2021, 0, 1, 13)).valueOf()); - - hook.rerender({ rangeFrom: 'now-15m', rangeTo: 'now' }); - - expect(hook.result.current.start).toEqual('2021-01-01T11:45:00.000Z'); - expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); - hook.rerender({ rangeFrom: 'now-30m', rangeTo: 'now' }); - expect(hook.result.current.start).toEqual('2021-01-01T12:30:00.000Z'); - expect(hook.result.current.end).toEqual('2021-01-01T13:00:00.000Z'); - }); + expect(hook.result.current.start).toEqual('2021-01-01T11:30:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); - it('updates when refreshTimeRange is called', async () => { Date.now = jest.fn(() => new Date(Date.UTC(2021, 0, 1, 13)).valueOf()); - hook.rerender({ rangeFrom: 'now-15m', rangeTo: 'now' }); + hook.rerender({ rangeFrom: 'now-30m', rangeTo: 'now' }); - expect(hook.result.current.start).toEqual('2021-01-01T11:45:00.000Z'); + // times should not change, because rangeFrom/rangeTo did not change + expect(hook.result.current.start).toEqual('2021-01-01T11:30:00.000Z'); expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); - act(() => { - hook.result.current.refreshTimeRange(); - }); + hook.rerender({ rangeFrom: 'now-30m', rangeTo: 'now-15m' }); + + expect(hook.result.current.start).toEqual('2021-01-01T12:30:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T12:45:00.000Z'); + + hook.rerender({ rangeFrom: 'now-45m', rangeTo: 'now-30m' }); - expect(hook.result.current.start).toEqual('2021-01-01T12:45:00.000Z'); - expect(hook.result.current.end).toEqual('2021-01-01T13:00:00.000Z'); + expect(hook.result.current.start).toEqual('2021-01-01T12:15:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T12:30:00.000Z'); }); }); diff --git a/x-pack/plugins/apm/public/hooks/use_time_range.ts b/x-pack/plugins/apm/public/hooks/use_time_range.ts index 8263767a402dd..940a83652addd 100644 --- a/x-pack/plugins/apm/public/hooks/use_time_range.ts +++ b/x-pack/plugins/apm/public/hooks/use_time_range.ts @@ -6,7 +6,7 @@ */ import { isEqual } from 'lodash'; -import { useCallback, useRef, useState } from 'react'; +import { useRef } from 'react'; import { getDateRange } from '../context/url_params_context/helpers'; export function useTimeRange({ @@ -18,24 +18,19 @@ export function useTimeRange({ }) { const rangeRef = useRef({ rangeFrom, rangeTo }); - const [timeRangeId, setTimeRangeId] = useState(0); - const stateRef = useRef(getDateRange({ state: {}, rangeFrom, rangeTo })); - const updateParsedTime = useCallback(() => { + const updateParsedTime = () => { stateRef.current = getDateRange({ state: {}, rangeFrom, rangeTo }); - }, [rangeFrom, rangeTo]); + }; if (!isEqual(rangeRef.current, { rangeFrom, rangeTo })) { updateParsedTime(); } - const { start, end } = stateRef.current; + rangeRef.current = { rangeFrom, rangeTo }; - const refreshTimeRange = useCallback(() => { - updateParsedTime(); - setTimeRangeId((id) => id + 1); - }, [setTimeRangeId, updateParsedTime]); + const { start, end } = stateRef.current; if (!start || !end) { throw new Error('start and/or end were unexpectedly not set'); @@ -44,7 +39,5 @@ export function useTimeRange({ return { start, end, - refreshTimeRange, - timeRangeId, }; } diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts index 7bf01e976e923..870dc8030d70b 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts @@ -5,112 +5,154 @@ * 2.0. */ -import { flatten, omit, isEmpty } from 'lodash'; -import { useHistory } from 'react-router-dom'; -import { useFetcher } from './use_fetcher'; -import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; -import { maybe } from '../../common/utils/maybe'; -import { APIReturnType } from '../services/rest/createCallApmApi'; -import { useUrlParams } from '../context/url_params_context/use_url_params'; -import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; +import { useRef, useState } from 'react'; +import type { Subscription } from 'rxjs'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + isCompleteResponse, + isErrorResponse, +} from '../../../../../src/plugins/data/public'; +import type { + HistogramItem, + SearchServiceParams, + SearchServiceValue, +} from '../../common/search_strategies/correlations/types'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../plugin'; -type APIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/distribution'>; - -const INITIAL_DATA = { - buckets: [] as APIResponse['buckets'], - noHits: true, - bucketSize: 0, -}; +interface RawResponse { + percentileThresholdValue?: number; + took: number; + values: SearchServiceValue[]; + overallHistogram: HistogramItem[]; + log: string[]; + ccsWarning: boolean; +} -export function useTransactionDistributionFetcher({ - transactionName, - kuery, - environment, -}: { - transactionName: string; - kuery: string; - environment: string; -}) { - const { serviceName, transactionType } = useApmServiceContext(); +interface TransactionDistributionFetcherState { + error?: Error; + isComplete: boolean; + isRunning: boolean; + loaded: number; + ccsWarning: RawResponse['ccsWarning']; + log: RawResponse['log']; + transactionDistribution?: RawResponse['overallHistogram']; + percentileThresholdValue?: RawResponse['percentileThresholdValue']; + timeTook?: number; + total: number; +} +export function useTransactionDistributionFetcher() { const { - urlParams: { start, end, transactionId, traceId }, - } = useUrlParams(); + services: { data }, + } = useKibana(); - const history = useHistory(); - const { data = INITIAL_DATA, status, error } = useFetcher( - async (callApmApi) => { - if (serviceName && start && end && transactionType && transactionName) { - const response = await callApmApi({ - endpoint: - 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', - params: { - path: { - serviceName, - }, - query: { - environment, - kuery, - start, - end, - transactionType, - transactionName, - transactionId, - traceId, - }, - }, - }); + const [ + fetchState, + setFetchState, + ] = useState({ + isComplete: false, + isRunning: false, + loaded: 0, + ccsWarning: false, + log: [], + total: 100, + }); - const selectedSample = - transactionId && traceId - ? flatten(response.buckets.map((bucket) => bucket.samples)).find( - (sample) => - sample.transactionId === transactionId && - sample.traceId === traceId - ) - : undefined; + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); - if (!selectedSample) { - // selected sample was not found. select a new one: - // sorted by total number of requests, but only pick - // from buckets that have samples - const bucketsSortedByCount = response.buckets - .filter((bucket) => !isEmpty(bucket.samples)) - .sort((bucket) => bucket.count); + function setResponse(response: IKibanaSearchResponse) { + setFetchState((prevState) => ({ + ...prevState, + isRunning: response.isRunning || false, + ccsWarning: response.rawResponse?.ccsWarning ?? false, + histograms: response.rawResponse?.values ?? [], + log: response.rawResponse?.log ?? [], + loaded: response.loaded!, + total: response.total!, + timeTook: response.rawResponse.took, + // only set percentileThresholdValue and overallHistogram once it's repopulated on a refresh, + // otherwise the consuming chart would flicker with an empty state on reload. + ...(response.rawResponse?.percentileThresholdValue !== undefined && + response.rawResponse?.overallHistogram !== undefined + ? { + transactionDistribution: response.rawResponse?.overallHistogram, + percentileThresholdValue: + response.rawResponse?.percentileThresholdValue, + } + : {}), + })); + } - const preferredSample = maybe(bucketsSortedByCount[0]?.samples[0]); + const startFetch = ( + params: Omit + ) => { + setFetchState((prevState) => ({ + ...prevState, + error: undefined, + isComplete: false, + })); + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); - history.replace({ - ...history.location, - search: fromQuery({ - ...omit(toQuery(history.location.search), [ - 'traceId', - 'transactionId', - ]), - ...preferredSample, - }), - }); - } + const searchServiceParams: SearchServiceParams = { + ...params, + analyzeCorrelations: false, + }; + const req = { params: searchServiceParams }; - return response; - } - }, - // the histogram should not be refetched if the transactionId or traceId changes - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - environment, - kuery, - serviceName, - start, - end, - transactionType, - transactionName, - ] - ); + // Submit the search request using the `data.search` service. + searchSubscription$.current = data.search + .search>(req, { + strategy: 'apmCorrelationsSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (res: IKibanaSearchResponse) => { + setResponse(res); + if (isCompleteResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + isRunnning: false, + isComplete: true, + })); + } else if (isErrorResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + error: (res as unknown) as Error, + setIsRunning: false, + })); + } + }, + error: (error: Error) => { + setFetchState((prevState) => ({ + ...prevState, + error, + setIsRunning: false, + })); + }, + }); + }; + + const cancelFetch = () => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + setFetchState((prevState) => ({ + ...prevState, + setIsRunning: false, + })); + }; return { - distributionData: data, - distributionStatus: status, - distributionError: error, + ...fetchState, + progress: fetchState.loaded / fetchState.total, + startFetch, + cancelFetch, }; } diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts new file mode 100644 index 0000000000000..49f2a279f4931 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef, useState } from 'react'; +import type { Subscription } from 'rxjs'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + isCompleteResponse, + isErrorResponse, +} from '../../../../../src/plugins/data/public'; +import type { + HistogramItem, + SearchServiceParams, + SearchServiceValue, +} from '../../common/search_strategies/correlations/types'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../plugin'; + +interface RawResponse { + percentileThresholdValue?: number; + took: number; + values: SearchServiceValue[]; + overallHistogram: HistogramItem[]; + log: string[]; + ccsWarning: boolean; +} + +interface TransactionLatencyCorrelationsFetcherState { + error?: Error; + isComplete: boolean; + isRunning: boolean; + loaded: number; + ccsWarning: RawResponse['ccsWarning']; + histograms: RawResponse['values']; + log: RawResponse['log']; + overallHistogram?: RawResponse['overallHistogram']; + percentileThresholdValue?: RawResponse['percentileThresholdValue']; + timeTook?: number; + total: number; +} + +export const useTransactionLatencyCorrelationsFetcher = () => { + const { + services: { data }, + } = useKibana(); + + const [ + fetchState, + setFetchState, + ] = useState({ + isComplete: false, + isRunning: false, + loaded: 0, + ccsWarning: false, + histograms: [], + log: [], + total: 100, + }); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + + function setResponse(response: IKibanaSearchResponse) { + setFetchState((prevState) => ({ + ...prevState, + isRunning: response.isRunning || false, + ccsWarning: response.rawResponse?.ccsWarning ?? false, + histograms: response.rawResponse?.values ?? [], + log: response.rawResponse?.log ?? [], + loaded: response.loaded!, + total: response.total!, + timeTook: response.rawResponse.took, + // only set percentileThresholdValue and overallHistogram once it's repopulated on a refresh, + // otherwise the consuming chart would flicker with an empty state on reload. + ...(response.rawResponse?.percentileThresholdValue !== undefined && + response.rawResponse?.overallHistogram !== undefined + ? { + overallHistogram: response.rawResponse?.overallHistogram, + percentileThresholdValue: + response.rawResponse?.percentileThresholdValue, + } + : {}), + })); + } + + const startFetch = ( + params: Omit + ) => { + setFetchState((prevState) => ({ + ...prevState, + error: undefined, + isComplete: false, + })); + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + + const searchServiceParams: SearchServiceParams = { + ...params, + analyzeCorrelations: true, + }; + const req = { params: searchServiceParams }; + + // Submit the search request using the `data.search` service. + searchSubscription$.current = data.search + .search>(req, { + strategy: 'apmCorrelationsSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (res: IKibanaSearchResponse) => { + setResponse(res); + if (isCompleteResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + isRunnning: false, + isComplete: true, + })); + } else if (isErrorResponse(res)) { + searchSubscription$.current?.unsubscribe(); + setFetchState((prevState) => ({ + ...prevState, + error: (res as unknown) as Error, + setIsRunning: false, + })); + } + }, + error: (error: Error) => { + setFetchState((prevState) => ({ + ...prevState, + error, + setIsRunning: false, + })); + }, + }); + }; + + const cancelFetch = () => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + setFetchState((prevState) => ({ + ...prevState, + setIsRunning: false, + })); + }; + + return { + ...fetchState, + progress: fetchState.loaded / fetchState.total, + startFetch, + cancelFetch, + }; +}; diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts new file mode 100644 index 0000000000000..673c1086033b5 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_transaction_trace_samples_fetcher.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useFetcher } from './use_fetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; + +export interface TraceSample { + traceId: string; + transactionId: string; +} + +const INITIAL_DATA = { + noHits: true, + traceSamples: [] as TraceSample[], +}; + +export function useTransactionTraceSamplesFetcher({ + transactionName, + kuery, + environment, +}: { + transactionName: string; + kuery: string; + environment: string; +}) { + const { serviceName, transactionType } = useApmServiceContext(); + + const { + urlParams: { + start, + end, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + }, + } = useUrlParams(); + + const { data = INITIAL_DATA, status, error } = useFetcher( + async (callApmApi) => { + if (serviceName && start && end && transactionType && transactionName) { + const response = await callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/traces/samples', + params: { + path: { + serviceName, + }, + query: { + environment, + kuery, + start, + end, + transactionType, + transactionName, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + }, + }, + }); + + if (response.noHits) { + return response; + } + + const { traceSamples } = response; + + return { + noHits: false, + traceSamples, + }; + } + }, + // the samples should not be refetched if the transactionId or traceId changes + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + environment, + kuery, + serviceName, + start, + end, + transactionType, + transactionName, + sampleRangeFrom, + sampleRangeTo, + ] + ); + + return { + traceSamplesData: data, + traceSamplesStatus: status, + traceSamplesError: error, + }; +} diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index e38262773b6db..7d49833c01abf 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -12,15 +12,13 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import type { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, - ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_TYPED, ALERT_REASON as ALERT_REASON_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, - ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_NON_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, ALERT_REASON as ALERT_REASON_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; @@ -51,8 +49,7 @@ import { const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; -const ALERT_SEVERITY_VALUE: typeof ALERT_SEVERITY_VALUE_TYPED = ALERT_SEVERITY_VALUE_NON_TYPED; +const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; const paramsSchema = schema.object({ @@ -258,8 +255,7 @@ export function registerTransactionDurationAnomalyAlertType({ ...getEnvironmentEsField(environment), [TRANSACTION_TYPE]: transactionType, [PROCESSOR_EVENT]: ProcessorEvent.transaction, - [ALERT_SEVERITY_LEVEL]: severityLevel, - [ALERT_SEVERITY_VALUE]: score, + [ALERT_SEVERITY]: severityLevel, [ALERT_EVALUATION_VALUE]: score, [ALERT_EVALUATION_THRESHOLD]: threshold, [ALERT_REASON]: formatTransactionDurationAnomalyReason({ diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts index ae42a0c94fe9c..e9986bd9f0cf5 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts @@ -49,14 +49,14 @@ export const asyncSearchServiceProvider = ( // 95th percentile to be displayed as a marker in the log log chart const { totalDocs, - percentiles: percentileThreshold, + percentiles: percentilesResponseThresholds, } = await fetchTransactionDurationPercentiles( esClient, params, params.percentileThreshold ? [params.percentileThreshold] : undefined ); const percentileThresholdValue = - percentileThreshold[`${params.percentileThreshold}.0`]; + percentilesResponseThresholds[`${params.percentileThreshold}.0`]; state.setPercentileThresholdValue(percentileThresholdValue); addLogMessage( @@ -107,11 +107,31 @@ export const asyncSearchServiceProvider = ( return; } + // finish early if correlation analysis is not required. + if (params.analyzeCorrelations === false) { + addLogMessage( + `Finish service since correlation analysis wasn't requested.` + ); + state.setProgress({ + loadedHistogramStepsize: 1, + loadedOverallHistogram: 1, + loadedFieldCanditates: 1, + loadedFieldValuePairs: 1, + loadedHistograms: 1, + }); + state.setIsRunning(false); + return; + } + // Create an array of ranges [2, 4, 6, ..., 98] - const percents = Array.from(range(2, 100, 2)); + const percentileAggregationPercents = range(2, 100, 2); const { percentiles: percentilesRecords, - } = await fetchTransactionDurationPercentiles(esClient, params, percents); + } = await fetchTransactionDurationPercentiles( + esClient, + params, + percentileAggregationPercents + ); const percentiles = Object.values(percentilesRecords); addLogMessage(`Loaded percentiles.`); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts index 4b10ceb035e15..3be3438b2d18f 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts @@ -49,7 +49,6 @@ describe('correlations', () => { end: '2021', environment: 'dev', kuery: '', - percentileThresholdValue: 75, includeFrozen: false, }, }); @@ -85,13 +84,6 @@ describe('correlations', () => { 'transaction.name': 'actualTransactionName', }, }, - { - range: { - 'transaction.duration.us': { - gte: 75, - }, - }, - }, ], }, }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts index f28556f7a90b5..8bd9f3d4e582c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts @@ -10,28 +10,11 @@ import { getOrElse } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { rangeRt } from '../../../../routes/default_api_types'; import { getCorrelationsFilters } from '../../../correlations/get_filters'; import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; -const getPercentileThresholdValueQuery = ( - percentileThresholdValue: number | undefined -): estypes.QueryDslQueryContainer[] => { - return percentileThresholdValue - ? [ - { - range: { - [TRANSACTION_DURATION]: { - gte: percentileThresholdValue, - }, - }, - }, - ] - : []; -}; - export const getTermsQuery = ( fieldName: string | undefined, fieldValue: string | undefined @@ -55,7 +38,6 @@ export const getQueryWithParams = ({ serviceName, start, end, - percentileThresholdValue, transactionType, transactionName, } = params; @@ -82,7 +64,6 @@ export const getQueryWithParams = ({ filter: [ ...filters, ...getTermsQuery(fieldName, fieldValue), - ...getPercentileThresholdValueQuery(percentileThresholdValue), ] as estypes.QueryDslQueryContainer[], }, }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts new file mode 100644 index 0000000000000..9afe9d916b38e --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from 'src/core/server'; +import { chunk } from 'lodash'; +import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; +import { asyncSearchServiceLogProvider } from '../correlations/async_search_service_log'; +import { asyncErrorCorrelationsSearchServiceStateProvider } from './async_search_service_state'; +import { fetchTransactionDurationFieldCandidates } from '../correlations/queries'; +import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { fetchFailedTransactionsCorrelationPValues } from './queries/query_failure_correlation'; +import { ERROR_CORRELATION_THRESHOLD } from './constants'; + +export const asyncErrorCorrelationSearchServiceProvider = ( + esClient: ElasticsearchClient, + getApmIndices: () => Promise, + searchServiceParams: SearchServiceParams, + includeFrozen: boolean +) => { + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + + const state = asyncErrorCorrelationsSearchServiceStateProvider(); + + async function fetchErrorCorrelations() { + try { + const indices = await getApmIndices(); + const params: SearchServiceFetchParams = { + ...searchServiceParams, + index: indices['apm_oss.transactionIndices'], + includeFrozen, + }; + + const { fieldCandidates } = await fetchTransactionDurationFieldCandidates( + esClient, + params + ); + + addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); + + state.setProgress({ loadedFieldCandidates: 1 }); + + let fieldCandidatesFetchedCount = 0; + if (params !== undefined && fieldCandidates.length > 0) { + const batches = chunk(fieldCandidates, 10); + for (let i = 0; i < batches.length; i++) { + try { + const results = await Promise.allSettled( + batches[i].map((fieldName) => + fetchFailedTransactionsCorrelationPValues( + esClient, + params, + fieldName + ) + ) + ); + + results.forEach((result, idx) => { + if (result.status === 'fulfilled') { + state.addValues( + result.value.filter( + (record) => + record && + typeof record.pValue === 'number' && + record.pValue < ERROR_CORRELATION_THRESHOLD + ) + ); + } else { + // If one of the fields in the batch had an error + addLogMessage( + `Error getting error correlation for field ${batches[i][idx]}: ${result.reason}.` + ); + } + }); + } catch (e) { + state.setError(e); + + if (params?.index.includes(':')) { + state.setCcsWarning(true); + } + } finally { + fieldCandidatesFetchedCount += batches[i].length; + state.setProgress({ + loadedErrorCorrelations: + fieldCandidatesFetchedCount / fieldCandidates.length, + }); + } + } + + addLogMessage( + `Identified correlations for ${fieldCandidatesFetchedCount} fields out of ${fieldCandidates.length} candidates.` + ); + } + } catch (e) { + state.setError(e); + } + + addLogMessage( + `Identified ${ + state.getState().values.length + } significant correlations relating to failed transactions.` + ); + + state.setIsRunning(false); + } + + fetchErrorCorrelations(); + + return () => { + const { ccsWarning, error, isRunning, progress } = state.getState(); + + return { + ccsWarning, + error, + log: getLogMessages(), + isRunning, + loaded: Math.round(state.getOverallProgress() * 100), + started: progress.started, + total: 100, + values: state.getValuesSortedByScore(), + cancel: () => { + addLogMessage(`Service cancelled.`); + state.setIsCancelled(true); + }, + }; + }; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service_state.ts new file mode 100644 index 0000000000000..fb0c6fea4879a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service_state.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; + +interface Progress { + started: number; + loadedFieldCandidates: number; + loadedErrorCorrelations: number; +} +export const asyncErrorCorrelationsSearchServiceStateProvider = () => { + let ccsWarning = false; + function setCcsWarning(d: boolean) { + ccsWarning = d; + } + + let error: Error; + function setError(d: Error) { + error = d; + } + + let isCancelled = false; + function setIsCancelled(d: boolean) { + isCancelled = d; + } + + let isRunning = true; + function setIsRunning(d: boolean) { + isRunning = d; + } + + let progress: Progress = { + started: Date.now(), + loadedFieldCandidates: 0, + loadedErrorCorrelations: 0, + }; + function getOverallProgress() { + return ( + progress.loadedFieldCandidates * 0.025 + + progress.loadedErrorCorrelations * (1 - 0.025) + ); + } + function setProgress(d: Partial>) { + progress = { + ...progress, + ...d, + }; + } + + const values: FailedTransactionsCorrelationValue[] = []; + function addValue(d: FailedTransactionsCorrelationValue) { + values.push(d); + } + function addValues(d: FailedTransactionsCorrelationValue[]) { + values.push(...d); + } + + function getValuesSortedByScore() { + return values.sort((a, b) => b.score - a.score); + } + + function getState() { + return { + ccsWarning, + error, + isCancelled, + isRunning, + progress, + values, + }; + } + + return { + addValue, + addValues, + getOverallProgress, + getState, + getValuesSortedByScore, + setCcsWarning, + setError, + setIsCancelled, + setIsRunning, + setProgress, + }; +}; + +export type AsyncSearchServiceState = ReturnType< + typeof asyncErrorCorrelationsSearchServiceStateProvider +>; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/constants.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/constants.ts new file mode 100644 index 0000000000000..711c5f736d774 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ERROR_CORRELATION_THRESHOLD = 0.02; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts new file mode 100644 index 0000000000000..f7e24ac6e1335 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { apmFailedTransactionsCorrelationsSearchStrategyProvider } from './search_strategy'; +export { FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY } from '../../../../common/search_strategies/failure_correlations/constants'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts new file mode 100644 index 0000000000000..22424d68f07ff --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { estypes } from '@elastic/elasticsearch'; +import { ElasticsearchClient } from 'kibana/server'; +import { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { + getQueryWithParams, + getTermsQuery, +} from '../../correlations/queries/get_query_with_params'; +import { getRequestBase } from '../../correlations/queries/get_request_base'; +import { EVENT_OUTCOME } from '../../../../../common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../../../common/event_outcome'; + +export const getFailureCorrelationRequest = ( + params: SearchServiceFetchParams, + fieldName: string +): estypes.SearchRequest => { + const query = getQueryWithParams({ + params, + }); + + const queryWithFailure = { + ...query, + bool: { + ...query.bool, + filter: [ + ...query.bool.filter, + ...getTermsQuery(EVENT_OUTCOME, EventOutcome.failure), + ], + }, + }; + + const body = { + query: queryWithFailure, + size: 0, + aggs: { + failure_p_value: { + significant_terms: { + field: fieldName, + background_filter: { + // Important to have same query as above here + // without it, we would be comparing sets of different filtered elements + ...query, + }, + // No need to have must_not "event.outcome": "failure" clause + // if background_is_superset is set to true + p_value: { background_is_superset: true }, + }, + }, + }, + }; + + return { + ...getRequestBase(params), + body, + }; +}; + +export const fetchFailedTransactionsCorrelationPValues = async ( + esClient: ElasticsearchClient, + params: SearchServiceFetchParams, + fieldName: string +) => { + const resp = await esClient.search( + getFailureCorrelationRequest(params, fieldName) + ); + + if (resp.body.aggregations === undefined) { + throw new Error( + 'fetchErrorCorrelation failed, did not return aggregations.' + ); + } + + const result = (resp.body.aggregations + .failure_p_value as estypes.AggregationsMultiBucketAggregate<{ + key: string; + doc_count: number; + bg_count: number; + score: number; + }>).buckets.map((b) => { + const score = b.score; + + // Scale the score into a value from 0 - 1 + // using a concave piecewise linear function in -log(p-value) + const normalizedScore = + 0.5 * Math.min(Math.max((score - 3.912) / 2.995, 0), 1) + + 0.25 * Math.min(Math.max((score - 6.908) / 6.908, 0), 1) + + 0.25 * Math.min(Math.max((score - 13.816) / 101.314, 0), 1); + + return { + ...b, + fieldName, + fieldValue: b.key, + pValue: Math.exp(-score), + normalizedScore, + }; + }); + + return result; +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/search_strategy.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/search_strategy.ts new file mode 100644 index 0000000000000..415f19e892741 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/search_strategy.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; +import { of } from 'rxjs'; + +import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../../../src/plugins/data/common'; + +import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; + +import { asyncErrorCorrelationSearchServiceProvider } from './async_search_service'; +import { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; + +export type PartialSearchRequest = IKibanaSearchRequest; +export type PartialSearchResponse = IKibanaSearchResponse<{ + values: FailedTransactionsCorrelationValue[]; +}>; + +export const apmFailedTransactionsCorrelationsSearchStrategyProvider = ( + getApmIndices: () => Promise, + includeFrozen: boolean +): ISearchStrategy => { + const asyncSearchServiceMap = new Map< + string, + ReturnType + >(); + + return { + search: (request, options, deps) => { + if (request.params === undefined) { + throw new Error('Invalid request parameters.'); + } + + // The function to fetch the current state of the async search service. + // This will be either an existing service for a follow up fetch or a new one for new requests. + let getAsyncSearchServiceState: ReturnType< + typeof asyncErrorCorrelationSearchServiceProvider + >; + + // If the request includes an ID, we require that the async search service already exists + // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. + // This also avoids instantiating async search services when the service gets called with random IDs. + if (typeof request.id === 'string') { + const existingGetAsyncSearchServiceState = asyncSearchServiceMap.get( + request.id + ); + + if (typeof existingGetAsyncSearchServiceState === 'undefined') { + throw new Error( + `AsyncSearchService with ID '${request.id}' does not exist.` + ); + } + + getAsyncSearchServiceState = existingGetAsyncSearchServiceState; + } else { + getAsyncSearchServiceState = asyncErrorCorrelationSearchServiceProvider( + deps.esClient.asCurrentUser, + getApmIndices, + request.params, + includeFrozen + ); + } + + // Reuse the request's id or create a new one. + const id = request.id ?? uuid(); + + const { + ccsWarning, + error, + log, + isRunning, + loaded, + started, + total, + values, + } = getAsyncSearchServiceState(); + + if (error instanceof Error) { + asyncSearchServiceMap.delete(id); + throw error; + } else if (isRunning) { + asyncSearchServiceMap.set(id, getAsyncSearchServiceState); + } else { + asyncSearchServiceMap.delete(id); + } + + const took = Date.now() - started; + + return of({ + id, + loaded, + total, + isRunning, + isPartial: isRunning, + rawResponse: { + ccsWarning, + log, + took, + values, + }, + }); + }, + cancel: async (id, options, deps) => { + const getAsyncSearchServiceState = asyncSearchServiceMap.get(id); + if (getAsyncSearchServiceState !== undefined) { + getAsyncSearchServiceState().cancel(); + asyncSearchServiceMap.delete(id); + } + }, + }; +}; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index be664529abab4..1b5df64dd8d00 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -63,14 +63,10 @@ Object { ], }, "body": Object { - "aggs": Object { - "agents": Object { - "terms": Object { - "field": "agent.name", - "size": 1, - }, - }, - }, + "_source": Array [ + "service.runtime.name", + "agent.name", + ], "query": Object { "bool": Object { "filter": Array [ @@ -88,10 +84,20 @@ Object { }, }, }, + Object { + "exists": Object { + "field": "service.runtime.name", + }, + }, + Object { + "exists": Object { + "field": "agent.name", + }, + }, ], }, }, - "size": 0, + "size": 1, }, "terminateAfter": 1, } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent.ts similarity index 64% rename from x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts rename to x-pack/plugins/apm/server/lib/services/get_service_agent.ts index 49489f2b33888..2a6ec74bc0d1a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent.ts @@ -9,12 +9,24 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { AGENT_NAME, SERVICE_NAME, + SERVICE_RUNTIME_NAME, } from '../../../common/elasticsearch_fieldnames'; import { rangeQuery } from '../../../../observability/server'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -export async function getServiceAgentName({ +interface ServiceAgent { + service?: { + runtime: { + name: string; + }; + }; + agent?: { + name: string; + }; +} + +export async function getServiceAgent({ serviceName, setup, searchAggregatedTransactions, @@ -37,27 +49,37 @@ export async function getServiceAgentName({ ], }, body: { - size: 0, + size: 1, + _source: [SERVICE_RUNTIME_NAME, AGENT_NAME], query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, ...rangeQuery(start, end), + { + exists: { + field: SERVICE_RUNTIME_NAME, + }, + }, + { + exists: { + field: AGENT_NAME, + }, + }, ], }, }, - aggs: { - agents: { - terms: { field: AGENT_NAME, size: 1 }, - }, - }, }, }; - const { aggregations } = await apmEventClient.search( + const response = await apmEventClient.search( 'get_service_agent_name', params ); - const agentName = aggregations?.agents.buckets[0]?.key as string | undefined; - return { agentName }; + if (response.hits.total.value === 0) { + return {}; + } + + const { service, agent } = response.hits.hits[0]._source as ServiceAgent; + return { agentName: agent?.name, runtimeName: service?.runtime.name }; } diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index a34382ddaf1fb..be5f280477a09 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getServiceAgentName } from './get_service_agent_name'; +import { getServiceAgent } from './get_service_agent'; import { getServiceTransactionTypes } from './get_service_transaction_types'; import { getServicesItems } from './get_services/get_services_items'; import { getLegacyDataStatus } from './get_services/get_legacy_data_status'; @@ -25,7 +25,7 @@ describe('services queries', () => { it('fetches the service agent name', async () => { mock = await inspectSearchParams((setup) => - getServiceAgentName({ + getServiceAgent({ serviceName: 'foo', setup, searchAggregatedTransactions: false, diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index baa9b3ae230fe..44125d557dcc8 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -335,7 +335,7 @@ Object { } `; -exports[`transaction queries fetches transaction distribution 1`] = ` +exports[`transaction queries fetches transaction trace samples 1`] = ` Object { "apm": Object { "events": Array [ @@ -343,13 +343,6 @@ Object { ], }, "body": Object { - "aggs": Object { - "stats": Object { - "max": Object { - "field": "transaction.duration.us", - }, - }, - }, "query": Object { "bool": Object { "filter": Array [ @@ -377,10 +370,27 @@ Object { }, }, }, + Object { + "term": Object { + "transaction.sampled": true, + }, + }, + ], + "should": Array [ + Object { + "term": Object { + "trace.id": "qux", + }, + }, + Object { + "term": Object { + "transaction.id": "quz", + }, + }, ], }, }, - "size": 0, + "size": 500, }, } `; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts deleted file mode 100644 index e868f7de049f9..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; -import { withApmSpan } from '../../../../utils/with_apm_span'; -import { - SERVICE_NAME, - TRACE_ID, - TRANSACTION_DURATION, - TRANSACTION_ID, - TRANSACTION_NAME, - TRANSACTION_SAMPLED, - TRANSACTION_TYPE, -} from '../../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../../common/processor_event'; -import { joinByKey } from '../../../../../common/utils/join_by_key'; -import { rangeQuery, kqlQuery } from '../../../../../../observability/server'; -import { environmentQuery } from '../../../../../common/utils/environment_query'; -import { - getDocumentTypeFilterForAggregatedTransactions, - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../../../helpers/aggregated_transactions'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; - -function getHistogramAggOptions({ - bucketSize, - field, - distributionMax, -}: { - bucketSize: number; - field: string; - distributionMax: number; -}) { - return { - field, - interval: bucketSize, - min_doc_count: 0, - extended_bounds: { - min: 0, - max: distributionMax, - }, - }; -} - -export async function getBuckets({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - distributionMax, - bucketSize, - setup, - searchAggregatedTransactions, -}: { - environment: string; - kuery: string; - serviceName: string; - transactionName: string; - transactionType: string; - transactionId: string; - traceId: string; - distributionMax: number; - bucketSize: number; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - return withApmSpan( - 'get_latency_distribution_buckets_with_samples', - async () => { - const { start, end, apmEventClient } = setup; - - const commonFilters = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [TRANSACTION_NAME]: transactionName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ] as QueryDslQueryContainer[]; - - async function getSamplesForDistributionBuckets() { - const response = await apmEventClient.search( - 'get_samples_for_latency_distribution_buckets', - { - apm: { - events: [ProcessorEvent.transaction], - }, - body: { - query: { - bool: { - filter: [ - ...commonFilters, - { term: { [TRANSACTION_SAMPLED]: true } }, - ], - should: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [TRANSACTION_ID]: transactionId } }, - ] as QueryDslQueryContainer[], - }, - }, - aggs: { - distribution: { - histogram: getHistogramAggOptions({ - bucketSize, - field: TRANSACTION_DURATION, - distributionMax, - }), - aggs: { - samples: { - top_hits: { - _source: [TRANSACTION_ID, TRACE_ID], - size: 10, - sort: { - _score: 'desc' as const, - }, - }, - }, - }, - }, - }, - }, - } - ); - - return ( - response.aggregations?.distribution.buckets.map((bucket) => { - const samples = bucket.samples.hits.hits; - return { - key: bucket.key, - samples: samples.map(({ _source: sample }) => ({ - traceId: sample.trace.id, - transactionId: sample.transaction.id, - })), - }; - }) ?? [] - ); - } - - async function getDistributionBuckets() { - const response = await apmEventClient.search( - 'get_latency_distribution_buckets', - { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - query: { - bool: { - filter: [ - ...commonFilters, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - }, - aggs: { - distribution: { - histogram: getHistogramAggOptions({ - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - bucketSize, - distributionMax, - }), - }, - }, - }, - } - ); - - return ( - response.aggregations?.distribution.buckets.map((bucket) => { - return { - key: bucket.key, - count: bucket.doc_count, - }; - }) ?? [] - ); - } - - const [ - samplesForDistributionBuckets, - distributionBuckets, - ] = await Promise.all([ - getSamplesForDistributionBuckets(), - getDistributionBuckets(), - ]); - - const buckets = joinByKey( - [...samplesForDistributionBuckets, ...distributionBuckets], - 'key' - ).map((bucket) => ({ - ...bucket, - samples: bucket.samples ?? [], - count: bucket.count ?? 0, - })); - - return { - noHits: buckets.length === 0, - bucketSize, - buckets, - }; - } - ); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts deleted file mode 100644 index 9c056bc506e92..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, -} from '../../helpers/aggregated_transactions'; -import { rangeQuery, kqlQuery } from '../../../../../observability/server'; -import { environmentQuery } from '../../../../common/utils/environment_query'; - -export async function getDistributionMax({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - setup, - searchAggregatedTransactions, -}: { - environment: string; - kuery: string; - serviceName: string; - transactionName: string; - transactionType: string; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - const { start, end, apmEventClient } = setup; - - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - { term: { [TRANSACTION_NAME]: transactionName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], - }, - }, - aggs: { - stats: { - max: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }; - - const resp = await apmEventClient.search( - 'get_latency_distribution_max', - params - ); - return resp.aggregations?.stats.value ?? null; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts deleted file mode 100644 index ef72f2434fde2..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { getBuckets } from './get_buckets'; -import { getDistributionMax } from './get_distribution_max'; -import { roundToNearestFiveOrTen } from '../../helpers/round_to_nearest_five_or_ten'; -import { MINIMUM_BUCKET_SIZE, BUCKET_TARGET_COUNT } from '../constants'; -import { withApmSpan } from '../../../utils/with_apm_span'; - -function getBucketSize(max: number) { - const bucketSize = max / BUCKET_TARGET_COUNT; - return roundToNearestFiveOrTen( - bucketSize > MINIMUM_BUCKET_SIZE ? bucketSize : MINIMUM_BUCKET_SIZE - ); -} - -export async function getTransactionDistribution({ - kuery, - environment, - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - setup, - searchAggregatedTransactions, -}: { - environment: string; - kuery: string; - serviceName: string; - transactionName: string; - transactionType: string; - transactionId: string; - traceId: string; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - return withApmSpan('get_transaction_latency_distribution', async () => { - const distributionMax = await getDistributionMax({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - setup, - searchAggregatedTransactions, - }); - - if (distributionMax == null) { - return { noHits: true, buckets: [], bucketSize: 0 }; - } - - const bucketSize = getBucketSize(distributionMax); - - const { buckets, noHits } = await getBuckets({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - transactionId, - traceId, - distributionMax, - bucketSize, - setup, - searchAggregatedTransactions, - }); - - return { - noHits, - buckets, - bucketSize, - }; - }); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts index b1d942a261387..b6b727d2273a1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts @@ -11,7 +11,7 @@ import { SearchParamsMock, } from '../../utils/test_helpers'; import { getTransactionBreakdown } from './breakdown'; -import { getTransactionDistribution } from './distribution'; +import { getTransactionTraceSamples } from './trace_samples'; import { getTransaction } from './get_transaction'; describe('transaction queries', () => { @@ -50,16 +50,15 @@ describe('transaction queries', () => { expect(mock.params).toMatchSnapshot(); }); - it('fetches transaction distribution', async () => { + it('fetches transaction trace samples', async () => { mock = await inspectSearchParams((setup) => - getTransactionDistribution({ + getTransactionTraceSamples({ serviceName: 'foo', transactionName: 'bar', transactionType: 'baz', traceId: 'qux', transactionId: 'quz', setup, - searchAggregatedTransactions: false, environment: ENVIRONMENT_ALL.value, kuery: '', }) diff --git a/x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.ts b/x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.ts new file mode 100644 index 0000000000000..98ef9ecaf346f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/trace_samples/get_trace_samples/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import { withApmSpan } from '../../../../utils/with_apm_span'; +import { + SERVICE_NAME, + TRACE_ID, + TRANSACTION_ID, + TRANSACTION_NAME, + TRANSACTION_SAMPLED, + TRANSACTION_TYPE, +} from '../../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { rangeQuery, kqlQuery } from '../../../../../../observability/server'; +import { environmentQuery } from '../../../../../common/utils/environment_query'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; + +const TRACE_SAMPLES_SIZE = 500; + +export async function getTraceSamples({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + setup, +}: { + environment: string; + kuery: string; + serviceName: string; + transactionName: string; + transactionType: string; + transactionId: string; + traceId: string; + sampleRangeFrom?: number; + sampleRangeTo?: number; + setup: Setup & SetupTimeRange; +}) { + return withApmSpan('get_trace_samples', async () => { + const { start, end, apmEventClient } = setup; + + const commonFilters = [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + { term: { [TRANSACTION_NAME]: transactionName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ] as QueryDslQueryContainer[]; + + if (sampleRangeFrom !== undefined && sampleRangeTo !== undefined) { + commonFilters.push({ + range: { + 'transaction.duration.us': { + gte: sampleRangeFrom, + lte: sampleRangeTo, + }, + }, + }); + } + + async function getTraceSamplesHits() { + const response = await apmEventClient.search('get_trace_samples_hits', { + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + ...commonFilters, + { term: { [TRANSACTION_SAMPLED]: true } }, + ], + should: [ + { term: { [TRACE_ID]: traceId } }, + { term: { [TRANSACTION_ID]: transactionId } }, + ] as QueryDslQueryContainer[], + }, + }, + size: TRACE_SAMPLES_SIZE, + }, + }); + + return response.hits.hits; + } + + const samplesForDistributionHits = await getTraceSamplesHits(); + + const traceSamples = samplesForDistributionHits.map((hit) => ({ + transactionId: hit._source.transaction.id, + traceId: hit._source.trace.id, + })); + + return { + noHits: samplesForDistributionHits.length === 0, + traceSamples, + }; + }); +} diff --git a/x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts b/x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts new file mode 100644 index 0000000000000..95548cd2afadf --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/trace_samples/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getTraceSamples } from './get_trace_samples'; +import { withApmSpan } from '../../../utils/with_apm_span'; + +export async function getTransactionTraceSamples({ + kuery, + environment, + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + setup, +}: { + environment: string; + kuery: string; + serviceName: string; + transactionName: string; + transactionType: string; + transactionId: string; + traceId: string; + sampleRangeFrom?: number; + sampleRangeTo?: number; + setup: Setup & SetupTimeRange; +}) { + return withApmSpan('get_transaction_trace_samples', async () => { + return await getTraceSamples({ + environment, + kuery, + serviceName, + transactionName, + transactionType, + transactionId, + traceId, + sampleRangeFrom, + sampleRangeTo, + setup, + }); + }); +} diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 807d21768a50c..1e0e61bc2bf3a 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -51,6 +51,10 @@ import { TRANSACTION_TYPE, } from '../common/elasticsearch_fieldnames'; import { tutorialProvider } from './tutorial'; +import { + apmFailedTransactionsCorrelationsSearchStrategyProvider, + FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY, +} from './lib/search_strategies/failed_transactions_correlations'; export class APMPlugin implements @@ -219,13 +223,25 @@ export class APMPlugin coreStart.savedObjects.createInternalRepository() ); + const includeFrozen = await coreStart.uiSettings + .asScopedToClient(savedObjectsClient) + .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); + + // Register APM latency correlations search strategy plugins.data.search.registerSearchStrategy( 'apmCorrelationsSearchStrategy', apmCorrelationsSearchStrategyProvider( boundGetApmIndices, - await coreStart.uiSettings - .asScopedToClient(savedObjectsClient) - .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN) + includeFrozen + ) + ); + + // Register APM failed transactions correlations search strategy + plugins.data.search.registerSearchStrategy( + FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY, + apmFailedTransactionsCorrelationsSearchStrategyProvider( + boundGetApmIndices, + includeFrozen ) ); })(); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index b4d185fecf5e2..32a7dcefb5cc8 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,7 +16,7 @@ import { getThroughputUnit } from '../lib/helpers/calculate_throughput'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceAnnotations } from '../lib/services/annotations'; import { getServices } from '../lib/services/get_services'; -import { getServiceAgentName } from '../lib/services/get_service_agent_name'; +import { getServiceAgent } from '../lib/services/get_service_agent'; import { getServiceAlerts } from '../lib/services/get_service_alerts'; import { getServiceDependencies } from '../lib/services/get_service_dependencies'; import { getServiceInstanceMetadataDetails } from '../lib/services/get_service_instance_metadata_details'; @@ -164,8 +164,8 @@ const serviceMetadataIconsRoute = createApmServerRoute({ }, }); -const serviceAgentNameRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/agent_name', +const serviceAgentRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/agent', params: t.type({ path: t.type({ serviceName: t.string, @@ -185,7 +185,7 @@ const serviceAgentNameRoute = createApmServerRoute({ kuery: '', }); - return getServiceAgentName({ + return getServiceAgent({ serviceName, setup, searchAggregatedTransactions, @@ -909,7 +909,7 @@ export const serviceRouteRepository = createApmServerRouteRepository() .add(servicesDetailedStatisticsRoute) .add(serviceMetadataDetailsRoute) .add(serviceMetadataIconsRoute) - .add(serviceAgentNameRoute) + .add(serviceAgentRoute) .add(serviceTransactionTypesRoute) .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index f211e722958c5..c267487cd36b7 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -16,7 +16,7 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; import { getServiceTransactionGroupDetailedStatisticsPeriods } from '../lib/services/get_service_transaction_group_detailed_statistics'; import { getTransactionBreakdown } from '../lib/transactions/breakdown'; -import { getTransactionDistribution } from '../lib/transactions/distribution'; +import { getTransactionTraceSamples } from '../lib/transactions/trace_samples'; import { getAnomalySeries } from '../lib/transactions/get_anomaly_data'; import { getLatencyPeriods } from '../lib/transactions/get_latency_charts'; import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate'; @@ -204,9 +204,8 @@ const transactionLatencyChartsRoute = createApmServerRoute({ }, }); -const transactionChartsDistributionRoute = createApmServerRoute({ - endpoint: - 'GET /api/apm/services/{serviceName}/transactions/charts/distribution', +const transactionTraceSamplesRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transactions/traces/samples', params: t.type({ path: t.type({ serviceName: t.string, @@ -219,6 +218,8 @@ const transactionChartsDistributionRoute = createApmServerRoute({ t.partial({ transactionId: t.string, traceId: t.string, + sampleRangeFrom: toNumberRt, + sampleRangeTo: toNumberRt, }), environmentRt, kueryRt, @@ -237,14 +238,11 @@ const transactionChartsDistributionRoute = createApmServerRoute({ transactionName, transactionId = '', traceId = '', + sampleRangeFrom, + sampleRangeTo, } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions({ - ...setup, - kuery, - }); - - return getTransactionDistribution({ + return getTransactionTraceSamples({ environment, kuery, serviceName, @@ -252,8 +250,9 @@ const transactionChartsDistributionRoute = createApmServerRoute({ transactionName, transactionId, traceId, + sampleRangeFrom, + sampleRangeTo, setup, - searchAggregatedTransactions, }); }, }); @@ -347,6 +346,6 @@ export const transactionRouteRepository = createApmServerRouteRepository() .add(transactionGroupsMainStatisticsRoute) .add(transactionGroupsDetailedStatisticsRoute) .add(transactionLatencyChartsRoute) - .add(transactionChartsDistributionRoute) + .add(transactionTraceSamplesRoute) .add(transactionChartsBreakdownRoute) .add(transactionChartsErrorRateRoute); diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 25113ccbb30df..f894ca23dfbf0 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -43,6 +43,16 @@ cases: CasesUiStart; cases.getCreateCase({ onCancel: handleSetIsCancel, onSuccess, + lensIntegration?: { + plugins: { + parsingPlugin, + processingPluginRenderer, + uiPlugin, + }, + hooks: { + useInsertTimeline, + }, + } timelineIntegration?: { plugins: { parsingPlugin, diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 3edbd3443ffc1..bf4ec0da6ee56 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -18,6 +18,12 @@ import { UserActionField, } from '../api'; +export interface CasesUiConfigType { + markdownPlugins: { + lens: boolean; + }; +} + export const StatusAll = 'all' as const; export type StatusAllType = typeof StatusAll; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/constants.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/constants.ts new file mode 100644 index 0000000000000..bc67e1b3228bb --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const LENS_ID = 'lens'; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/index.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/index.ts new file mode 100644 index 0000000000000..4f48da5838380 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './constants'; +export * from './parser'; +export * from './serializer'; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/parser.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/parser.ts new file mode 100644 index 0000000000000..58ebfd76d5ac5 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/parser.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Plugin } from 'unified'; +import { RemarkTokenizer } from '@elastic/eui'; +import { LENS_ID } from './constants'; + +export const LensParser: Plugin = function () { + const Parser = this.Parser; + const tokenizers = Parser.prototype.blockTokenizers; + const methods = Parser.prototype.blockMethods; + + const tokenizeLens: RemarkTokenizer = function (eat, value, silent) { + if (value.startsWith(`!{${LENS_ID}`) === false) return true; + + const nextChar = value[6]; + + if (nextChar !== '{' && nextChar !== '}') return false; // this isn't actually a lens + + if (silent) { + return true; + } + + // is there a configuration? + const hasConfiguration = nextChar === '{'; + + let match = `!{${LENS_ID}`; + let configuration = {}; + + if (hasConfiguration) { + let configurationString = ''; + + let openObjects = 0; + + for (let i = 6; i < value.length; i++) { + const char = value[i]; + if (char === '{') { + openObjects++; + configurationString += char; + } else if (char === '}') { + openObjects--; + if (openObjects === -1) { + break; + } + configurationString += char; + } else { + configurationString += char; + } + } + + match += configurationString; + try { + configuration = JSON.parse(configurationString); + } catch (e) { + const now = eat.now(); + this.file.fail(`Unable to parse lens JSON configuration: ${e}`, { + line: now.line, + column: now.column + 6, + }); + } + } + + match += '}'; + + return eat(match)({ + type: LENS_ID, + ...configuration, + }); + }; + + tokenizers.lens = tokenizeLens; + methods.splice(methods.indexOf('text'), 0, LENS_ID); +}; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts new file mode 100644 index 0000000000000..e561b2f8cfb8a --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TimeRange } from 'src/plugins/data/common'; +import { LENS_ID } from './constants'; + +export interface LensSerializerProps { + attributes: Record; + timeRange: TimeRange; +} + +export const LensSerializer = ({ timeRange, attributes }: LensSerializerProps) => + `!{${LENS_ID}${JSON.stringify({ + timeRange, + attributes, + })}}`; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/index.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/index.ts new file mode 100644 index 0000000000000..c6a22791db5f6 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './parser'; +export * from './serializer'; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/parser.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/parser.ts new file mode 100644 index 0000000000000..0decdae8c7348 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/parser.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Plugin } from 'unified'; +import { RemarkTokenizer } from '@elastic/eui'; +import * as i18n from './translations'; + +export const ID = 'timeline'; +const PREFIX = '['; + +export const TimelineParser: Plugin = function () { + const Parser = this.Parser; + const tokenizers = Parser.prototype.blockTokenizers; + const methods = Parser.prototype.blockMethods; + + const tokenizeTimeline: RemarkTokenizer = function (eat, value, silent) { + if ( + value.startsWith(PREFIX) === false || + (value.startsWith(PREFIX) === true && !value.includes('timelines?timeline=(id')) + ) { + return false; + } + + let index = 0; + const nextChar = value[index]; + + if (nextChar !== PREFIX) { + return false; + } + + if (silent) { + return true; + } + + function readArg(open: string, close: string) { + if (value[index] !== open) { + throw new Error(i18n.NO_PARENTHESES); + } + + index++; + + let body = ''; + let openBrackets = 0; + + for (; index < value.length; index++) { + const char = value[index]; + + if (char === close && openBrackets === 0) { + index++; + return body; + } else if (char === close) { + openBrackets--; + } else if (char === open) { + openBrackets++; + } + + body += char; + } + + return ''; + } + + const timelineTitle = readArg(PREFIX, ']'); + const timelineUrl = readArg('(', ')'); + const match = `[${timelineTitle}](${timelineUrl})`; + + return eat(match)({ + type: ID, + match, + }); + }; + + tokenizeTimeline.locator = (value: string, fromIndex: number) => { + return value.indexOf(PREFIX, fromIndex); + }; + + tokenizers.timeline = tokenizeTimeline; + methods.splice(methods.indexOf('url'), 0, ID); +}; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts new file mode 100644 index 0000000000000..0a95c9466b1ff --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface TimelineSerializerProps { + match: string; +} + +export const TimelineSerializer = ({ match }: TimelineSerializerProps) => match; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/translations.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/translations.ts new file mode 100644 index 0000000000000..a1244f0ae67aa --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/translations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_PARENTHESES = i18n.translate( + 'xpack.cases.markdownEditor.plugins.timeline.noParenthesesErrorMsg', + { + defaultMessage: 'Expected left parentheses', + } +); diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index f72f0e012bd80..ebac6295166df 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -20,11 +20,15 @@ "requiredPlugins":[ "actions", "esUiShared", + "lens", "features", "kibanaReact", "kibanaUtils", "triggersActionsUi" ], + "requiredBundles": [ + "savedObjects" + ], "server":true, "ui":true, "version":"8.0.0" diff --git a/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts index 392b71befe2b4..fb5e3f89d74b1 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts @@ -12,7 +12,11 @@ import { createWithKibanaMock, } from '../kibana_react.mock'; -export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; +export const KibanaServices = { + get: jest.fn(), + getKibanaVersion: jest.fn(() => '8.0.0'), + getConfig: jest.fn(() => null), +}; export const useKibana = jest.fn().mockReturnValue({ services: createStartServicesMock(), }); diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts index ff03782447846..e1990efefeffc 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts @@ -15,6 +15,13 @@ import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; import { securityMock } from '../../../../../security/public/mocks'; import { triggersActionsUiMock } from '../../../../../triggers_actions_ui/public/mocks'; +export const mockCreateStartServicesMock = (): StartServices => + (({ + ...coreMock.createStart(), + security: securityMock.createStart(), + triggersActionsUi: triggersActionsUiMock.createStart(), + } as unknown) as StartServices); + export const createStartServicesMock = (): StartServices => (({ ...coreMock.createStart(), diff --git a/x-pack/plugins/cases/public/common/lib/kibana/services.ts b/x-pack/plugins/cases/public/common/lib/kibana/services.ts index 94487bd3ca5e9..3a1f220d9794f 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/services.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/services.ts @@ -6,16 +6,23 @@ */ import { CoreStart } from 'kibana/public'; +import { CasesUiConfigType } from '../../../../common/ui/types'; type GlobalServices = Pick; export class KibanaServices { private static kibanaVersion?: string; private static services?: GlobalServices; + private static config?: CasesUiConfigType; - public static init({ http, kibanaVersion }: GlobalServices & { kibanaVersion: string }) { + public static init({ + http, + kibanaVersion, + config, + }: GlobalServices & { kibanaVersion: string; config: CasesUiConfigType }) { this.services = { http }; this.kibanaVersion = kibanaVersion; + this.config = config; } public static get(): GlobalServices { @@ -34,6 +41,10 @@ export class KibanaServices { return this.kibanaVersion; } + public static getConfig() { + return this.config; + } + private static throwUninitializedError(): never { throw new Error( 'Kibana services not initialized - are you trying to import this module from outside of the Cases app?' diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index db3f22a074d3b..06a3897687921 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -26,6 +26,7 @@ const onCommentPosted = jest.fn(); const postComment = jest.fn(); const addCommentProps: AddCommentProps = { + id: 'newComment', caseId: '1234', userCanCrud: true, onCommentSaving, diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 4ec06d6b55197..f788456a30dff 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -6,7 +6,7 @@ */ import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, forwardRef, useImperativeHandle } from 'react'; +import React, { useCallback, useRef, forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; import { CommentType } from '../../../common'; @@ -19,6 +19,7 @@ import * as i18n from './translations'; import { schema, AddCommentFormSchema } from './schema'; import { InsertTimeline } from '../insert_timeline'; import { useOwnerContext } from '../owner_context/use_owner_context'; + const MySpinner = styled(EuiLoadingSpinner)` position: absolute; top: 50%; @@ -31,9 +32,11 @@ const initialCommentValue: AddCommentFormSchema = { export interface AddCommentRefObject { addQuote: (quote: string) => void; + setComment: (newComment: string) => void; } export interface AddCommentProps { + id: string; caseId: string; userCanCrud?: boolean; onCommentSaving?: () => void; @@ -47,6 +50,7 @@ export const AddComment = React.memo( forwardRef( ( { + id, caseId, userCanCrud, onCommentPosted, @@ -57,6 +61,7 @@ export const AddComment = React.memo( }, ref ) => { + const editorRef = useRef(); const owner = useOwnerContext(); const { isLoading, postComment } = usePostComment(); @@ -77,8 +82,17 @@ export const AddComment = React.memo( [comment, setFieldValue] ); + const setComment = useCallback( + (newComment) => { + setFieldValue(fieldName, newComment); + }, + [setFieldValue] + ); + useImperativeHandle(ref, () => ({ addQuote, + setComment, + editor: editorRef.current, })); const onSubmit = useCallback(async () => { @@ -106,6 +120,8 @@ export const AddComment = React.memo( path={fieldName} component={MarkdownEditorForm} componentProps={{ + ref: editorRef, + id, idAria: 'caseComment', isDisabled: isLoading, dataTestSubj: 'add-comment', diff --git a/x-pack/plugins/cases/public/components/create/description.test.tsx b/x-pack/plugins/cases/public/components/create/description.test.tsx index fcd1f82d64a53..923c73193f992 100644 --- a/x-pack/plugins/cases/public/components/create/description.test.tsx +++ b/x-pack/plugins/cases/public/components/create/description.test.tsx @@ -12,6 +12,7 @@ import { act } from '@testing-library/react'; import { useForm, Form, FormHook } from '../../common/shared_imports'; import { Description } from './description'; import { schema, FormProps } from './schema'; +jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment'); describe('Description', () => { let globalForm: FormHook; diff --git a/x-pack/plugins/cases/public/components/create/description.tsx b/x-pack/plugins/cases/public/components/create/description.tsx index 0a7102cff1ad5..d11c64789c3f0 100644 --- a/x-pack/plugins/cases/public/components/create/description.tsx +++ b/x-pack/plugins/cases/public/components/create/description.tsx @@ -5,26 +5,43 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useEffect, useRef } from 'react'; import { MarkdownEditorForm } from '../markdown_editor'; -import { UseField } from '../../common/shared_imports'; +import { UseField, useFormContext } from '../../common/shared_imports'; +import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; + interface Props { isLoading: boolean; } export const fieldName = 'description'; -const DescriptionComponent: React.FC = ({ isLoading }) => ( - -); +const DescriptionComponent: React.FC = ({ isLoading }) => { + const { draftComment, openLensModal } = useLensDraftComment(); + const { setFieldValue } = useFormContext(); + const editorRef = useRef>(); + + useEffect(() => { + if (draftComment?.commentId === fieldName && editorRef.current) { + setFieldValue(fieldName, draftComment.comment); + openLensModal({ editorRef: editorRef.current }); + } + }, [draftComment, openLensModal, setFieldValue]); + + return ( + + ); +}; DescriptionComponent.displayName = 'DescriptionComponent'; diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 783ead9b271fd..9c3071fe27ee5 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -23,6 +23,7 @@ import { useCaseConfigureResponse } from '../configure_cases/__mock__'; jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/configure/use_configure'); +jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment'); const useGetTagsMock = useGetTags as jest.Mock; const useConnectorsMock = useConnectors as jest.Mock; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/context.tsx b/x-pack/plugins/cases/public/components/markdown_editor/context.tsx new file mode 100644 index 0000000000000..d7f5b0612cb73 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/context.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +export const CommentEditorContext = React.createContext<{ + editorId: string; + value: string; +} | null>(null); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx index 4bd26678e41a2..64aac233f1bb9 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx @@ -5,15 +5,26 @@ * 2.0. */ -import React, { memo, useState, useCallback } from 'react'; +import React, { + memo, + forwardRef, + useCallback, + useMemo, + useRef, + useState, + useImperativeHandle, + ElementRef, +} from 'react'; import { PluggableList } from 'unified'; import { EuiMarkdownEditor, EuiMarkdownEditorUiPlugin } from '@elastic/eui'; +import { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context'; import { usePlugins } from './use_plugins'; +import { CommentEditorContext } from './context'; interface MarkdownEditorProps { ariaLabel: string; dataTestSubj?: string; - editorId?: string; + editorId: string; height?: number; onChange: (content: string) => void; parsingPlugins?: PluggableList; @@ -22,35 +33,64 @@ interface MarkdownEditorProps { value: string; } -const MarkdownEditorComponent: React.FC = ({ - ariaLabel, - dataTestSubj, - editorId, - height, - onChange, - value, -}) => { - const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); - const onParse = useCallback((err, { messages }) => { - setMarkdownErrorMessages(err ? [err] : messages); - }, []); - const { parsingPlugins, processingPlugins, uiPlugins } = usePlugins(); - - return ( - - ); -}; +type EuiMarkdownEditorRef = ElementRef; + +export interface MarkdownEditorRef { + textarea: HTMLTextAreaElement | null; + replaceNode: ContextShape['replaceNode']; + toolbar: HTMLDivElement | null; +} + +const MarkdownEditorComponent = forwardRef( + ({ ariaLabel, dataTestSubj, editorId, height, onChange, value }, ref) => { + const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); + const onParse = useCallback((err, { messages }) => { + setMarkdownErrorMessages(err ? [err] : messages); + }, []); + const { parsingPlugins, processingPlugins, uiPlugins } = usePlugins(); + const editorRef = useRef(null); + + const commentEditorContextValue = useMemo( + () => ({ + editorId, + value, + }), + [editorId, value] + ); + + // @ts-expect-error + useImperativeHandle(ref, () => { + if (!editorRef.current) { + return null; + } + + const editorNode = editorRef.current?.textarea?.closest('.euiMarkdownEditor'); + + return { + ...editorRef.current, + toolbar: editorNode?.querySelector('.euiMarkdownEditorToolbar'), + }; + }); + + return ( + + + + ); + } +); export const MarkdownEditor = memo(MarkdownEditorComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index c2b2e8c77cb38..2719f38f98fc2 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { forwardRef } from 'react'; import styled from 'styled-components'; import { EuiMarkdownEditorProps, EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; -import { MarkdownEditor } from './editor'; +import { MarkdownEditor, MarkdownEditorRef } from './editor'; type MarkdownEditorFormProps = EuiMarkdownEditorProps & { id: string; @@ -26,40 +26,39 @@ const BottomContentWrapper = styled(EuiFlexGroup)` `} `; -export const MarkdownEditorForm: React.FC = ({ - id, - field, - dataTestSubj, - idAria, - bottomRightContent, -}) => { - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); +export const MarkdownEditorForm = React.memo( + forwardRef( + ({ id, field, dataTestSubj, idAria, bottomRightContent }, ref) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - return ( - <> - - - - {bottomRightContent && ( - - {bottomRightContent} - - )} - - ); -}; + return ( + <> + + + + {bottomRightContent && ( + + {bottomRightContent} + + )} + + ); + } + ) +); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.ts new file mode 100644 index 0000000000000..a0f0d49b211fb --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const useLensDraftComment = () => ({}); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/constants.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/constants.ts new file mode 100644 index 0000000000000..05826f73fe007 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ID = 'lens'; +export const PREFIX = `[`; +export const LENS_VISUALIZATION_HEIGHT = 200; +export const DRAFT_COMMENT_STORAGE_ID = 'xpack.cases.commentDraft'; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/index.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/index.ts new file mode 100644 index 0000000000000..1d0bb2bf6c86e --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { plugin } from './plugin'; +import { LensParser } from './parser'; +import { LensMarkDownRenderer } from './processor'; +import { INSERT_LENS } from './translations'; + +export { plugin, LensParser as parser, LensMarkDownRenderer as renderer, INSERT_LENS }; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/modal_container.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/modal_container.tsx new file mode 100644 index 0000000000000..0f70e80deed41 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/modal_container.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; + +export const ModalContainer = styled.div` + width: ${({ theme }) => theme.eui.euiBreakpoints.m}; + + .euiModalBody { + min-height: 300px; + } +`; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/parser.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/parser.ts new file mode 100644 index 0000000000000..8d598fad260dc --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/parser.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Plugin } from 'unified'; +import { RemarkTokenizer } from '@elastic/eui'; +import { ID } from './constants'; + +export const LensParser: Plugin = function () { + const Parser = this.Parser; + const tokenizers = Parser.prototype.blockTokenizers; + const methods = Parser.prototype.blockMethods; + + const tokenizeLens: RemarkTokenizer = function (eat, value, silent) { + if (value.startsWith(`!{${ID}`) === false) return false; + + const nextChar = value[6]; + + if (nextChar !== '{' && nextChar !== '}') return false; // this isn't actually a lens + + if (silent) { + return true; + } + + // is there a configuration? + const hasConfiguration = nextChar === '{'; + + let match = `!{${ID}`; + let configuration = {}; + + if (hasConfiguration) { + let configurationString = ''; + + let openObjects = 0; + + for (let i = 6; i < value.length; i++) { + const char = value[i]; + if (char === '{') { + openObjects++; + configurationString += char; + } else if (char === '}') { + openObjects--; + if (openObjects === -1) { + break; + } + configurationString += char; + } else { + configurationString += char; + } + } + + match += configurationString; + try { + configuration = JSON.parse(configurationString); + } catch (e) { + const now = eat.now(); + this.file.fail(`Unable to parse lens JSON configuration: ${e}`, { + line: now.line, + column: now.column + 6, + }); + } + } + + match += '}'; + + return eat(match)({ + type: ID, + ...configuration, + }); + }; + + tokenizers.lens = tokenizeLens; + methods.splice(methods.indexOf('text'), 0, ID); +}; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx new file mode 100644 index 0000000000000..24dde054d2d19 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx @@ -0,0 +1,464 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { first } from 'rxjs/operators'; +import { + EuiFieldText, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiMarkdownEditorUiPlugin, + EuiMarkdownContext, + EuiCodeBlock, + EuiSpacer, + EuiModalFooter, + EuiButtonEmpty, + EuiButton, + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiMarkdownAstNodePosition, + EuiBetaBadge, +} from '@elastic/eui'; +import React, { ReactNode, useCallback, useContext, useMemo, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useLocation } from 'react-router-dom'; +import styled from 'styled-components'; + +import type { TypedLensByValueInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../common/lib/kibana'; +import { LensMarkDownRenderer } from './processor'; +import { DRAFT_COMMENT_STORAGE_ID, ID } from './constants'; +import { CommentEditorContext } from '../../context'; +import { ModalContainer } from './modal_container'; +import type { EmbeddablePackageState } from '../../../../../../../../src/plugins/embeddable/public'; +import { + SavedObjectFinderUi, + SavedObjectFinderUiProps, +} from '../../../../../../../../src/plugins/saved_objects/public'; +import { useLensDraftComment } from './use_lens_draft_comment'; + +const BetaBadgeWrapper = styled.span` + display: inline-flex; + + .euiToolTipAnchor { + display: inline-flex; + } +`; + +type LensIncomingEmbeddablePackage = Omit & { + input: TypedLensByValueInput; +}; + +type LensEuiMarkdownEditorUiPlugin = EuiMarkdownEditorUiPlugin<{ + title: string; + timeRange: TypedLensByValueInput['timeRange']; + startDate: string; + endDate: string; + position: EuiMarkdownAstNodePosition; + attributes: TypedLensByValueInput['attributes']; +}>; + +interface LensSavedObjectsPickerProps { + children: ReactNode; + onChoose: SavedObjectFinderUiProps['onChoose']; +} + +const LensSavedObjectsPickerComponent: React.FC = ({ + children, + onChoose, +}) => { + const { savedObjects, uiSettings } = useKibana().services; + + const savedObjectMetaData = useMemo( + () => [ + { + type: 'lens', + getIconForSavedObject: () => 'lensApp', + name: i18n.translate( + 'xpack.cases.markdownEditor.plugins.lens.insertLensSavedObjectModal.searchSelection.savedObjectType.lens', + { + defaultMessage: 'Lens', + } + ), + includeFields: ['*'], + }, + ], + [] + ); + + return ( + + } + savedObjectMetaData={savedObjectMetaData} + fixedPageSize={10} + uiSettings={uiSettings} + savedObjects={savedObjects} + children={children} + /> + ); +}; + +export const LensSavedObjectsPicker = React.memo(LensSavedObjectsPickerComponent); + +const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({ + node, + onCancel, + onSave, +}) => { + const location = useLocation(); + const { + application: { currentAppId$ }, + embeddable, + lens, + storage, + data: { + query: { + timefilter: { timefilter }, + }, + }, + } = useKibana().services; + const [currentAppId, setCurrentAppId] = useState(undefined); + + const { draftComment, clearDraftComment } = useLensDraftComment(); + + const [nodePosition, setNodePosition] = useState( + undefined + ); + // const [editMode, setEditMode] = useState(!!node); + const [lensEmbeddableAttributes, setLensEmbeddableAttributes] = useState< + TypedLensByValueInput['attributes'] | null + >(node?.attributes || null); + const [timeRange, setTimeRange] = useState( + node?.timeRange ?? { + from: 'now-7d', + to: 'now', + mode: 'relative', + } + ); + const commentEditorContext = useContext(CommentEditorContext); + const markdownContext = useContext(EuiMarkdownContext); + + const handleTitleChange = useCallback((e) => { + const title = e.target.value ?? ''; + setLensEmbeddableAttributes((currentValue) => { + if (currentValue) { + return { ...currentValue, title } as TypedLensByValueInput['attributes']; + } + + return currentValue; + }); + }, []); + + const handleClose = useCallback(() => { + if (currentAppId) { + embeddable?.getStateTransfer().getIncomingEmbeddablePackage(currentAppId, true); + clearDraftComment(); + } + onCancel(); + }, [clearDraftComment, currentAppId, embeddable, onCancel]); + + const handleAdd = useCallback(() => { + if (nodePosition) { + markdownContext.replaceNode( + nodePosition, + `!{${ID}${JSON.stringify({ + timeRange, + attributes: lensEmbeddableAttributes, + })}}` + ); + + handleClose(); + return; + } + + if (lensEmbeddableAttributes) { + onSave( + `!{${ID}${JSON.stringify({ + timeRange, + attributes: lensEmbeddableAttributes, + })}}`, + { + block: true, + } + ); + } + + handleClose(); + }, [nodePosition, lensEmbeddableAttributes, handleClose, markdownContext, timeRange, onSave]); + + const handleDelete = useCallback(() => { + if (nodePosition) { + markdownContext.replaceNode(nodePosition, ``); + onCancel(); + } + }, [markdownContext, nodePosition, onCancel]); + + const originatingPath = useMemo(() => `${location.pathname}${location.search}`, [ + location.pathname, + location.search, + ]); + + const handleEditInLensClick = useCallback( + async (lensAttributes?) => { + storage.set(DRAFT_COMMENT_STORAGE_ID, { + commentId: commentEditorContext?.editorId, + comment: commentEditorContext?.value, + position: node?.position, + title: lensEmbeddableAttributes?.title, + }); + + lens?.navigateToPrefilledEditor( + lensAttributes || lensEmbeddableAttributes + ? { + id: '', + timeRange, + attributes: lensAttributes ?? lensEmbeddableAttributes, + } + : undefined, + { + originatingApp: currentAppId!, + originatingPath, + } + ); + }, + [ + storage, + commentEditorContext?.editorId, + commentEditorContext?.value, + node?.position, + lens, + lensEmbeddableAttributes, + timeRange, + currentAppId, + originatingPath, + ] + ); + + const handleChooseLensSO = useCallback( + (savedObjectId, savedObjectType, fullName, savedObject) => { + handleEditInLensClick({ + ...savedObject.attributes, + title: '', + references: savedObject.references, + }); + }, + [handleEditInLensClick] + ); + + useEffect(() => { + if (node?.attributes) { + setLensEmbeddableAttributes(node.attributes); + } + }, [node?.attributes]); + + useEffect(() => { + const position = node?.position || draftComment?.position; + if (position) { + setNodePosition(position); + } + }, [node?.position, draftComment?.position]); + + useEffect(() => { + const getCurrentAppId = async () => { + const appId = await currentAppId$.pipe(first()).toPromise(); + setCurrentAppId(appId); + }; + getCurrentAppId(); + }, [currentAppId$]); + + useEffect(() => { + let incomingEmbeddablePackage; + + if (currentAppId) { + incomingEmbeddablePackage = embeddable + ?.getStateTransfer() + .getIncomingEmbeddablePackage(currentAppId, true) as LensIncomingEmbeddablePackage; + } + + if ( + incomingEmbeddablePackage?.type === 'lens' && + incomingEmbeddablePackage?.input?.attributes + ) { + const attributesTitle = incomingEmbeddablePackage?.input.attributes.title.length + ? incomingEmbeddablePackage?.input.attributes.title + : null; + setLensEmbeddableAttributes({ + ...incomingEmbeddablePackage?.input.attributes, + title: attributesTitle ?? draftComment?.title ?? '', + }); + + const lensTime = timefilter.getTime(); + if (lensTime?.from && lensTime?.to) { + setTimeRange({ + from: lensTime.from, + to: lensTime.to, + mode: [lensTime.from, lensTime.to].join('').includes('now') ? 'relative' : 'absolute', + }); + } + } + }, [embeddable, storage, timefilter, currentAppId, draftComment?.title]); + + return ( + + + + + + {!!nodePosition ? ( + + ) : ( + + )} + + + + + + + + + + + {lensEmbeddableAttributes ? ( + <> + + + + + + + + + + + + + + + + ) : ( + + + + + + + + )} + + + + + + {!!nodePosition ? ( + + + + ) : null} + + {!!nodePosition ? ( + + ) : ( + + )} + + + + ); +}; + +export const LensEditor = React.memo(LensEditorComponent); + +export const plugin: LensEuiMarkdownEditorUiPlugin = { + name: ID, + button: { + label: i18n.translate('xpack.cases.markdownEditor.plugins.lens.insertLensButtonLabel', { + defaultMessage: 'Insert visualization', + }), + iconType: 'lensApp', + }, + helpText: ( + + {'!{lens}'} + + ), + editor: LensEditor, +}; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx new file mode 100644 index 0000000000000..cc8ef07392670 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { first } from 'rxjs/operators'; +import React, { useCallback, useEffect, useState } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; +import styled from 'styled-components'; +import { useLocation } from 'react-router-dom'; + +import { createGlobalStyle } from '../../../../../../../../src/plugins/kibana_react/common'; +import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../common/lib/kibana'; +import { LENS_VISUALIZATION_HEIGHT } from './constants'; + +const Container = styled.div` + min-height: ${LENS_VISUALIZATION_HEIGHT}px; +`; + +// when displaying chart in modal the tooltip is render under the modal +const LensChartTooltipFix = createGlobalStyle` + div.euiOverlayMask.euiOverlayMask--aboveHeader ~ [id^='echTooltipPortal'] { + z-index: ${({ theme }) => theme.eui.euiZLevel7} !important; + } +`; + +interface LensMarkDownRendererProps { + attributes: TypedLensByValueInput['attributes'] | null; + id?: string | null; + timeRange?: TypedLensByValueInput['timeRange']; + startDate?: string | null; + endDate?: string | null; + viewMode?: boolean | undefined; +} + +const LensMarkDownRendererComponent: React.FC = ({ + attributes, + timeRange, + viewMode = true, +}) => { + const location = useLocation(); + const { + application: { currentAppId$ }, + lens: { EmbeddableComponent, navigateToPrefilledEditor, canUseEditor }, + } = useKibana().services; + const [currentAppId, setCurrentAppId] = useState(undefined); + + const handleClick = useCallback(() => { + const options = viewMode + ? { + openInNewTab: true, + } + : { + originatingApp: currentAppId, + originatingPath: `${location.pathname}${location.search}`, + }; + + if (attributes) { + navigateToPrefilledEditor( + { + id: '', + timeRange, + attributes, + }, + options + ); + } + }, [ + attributes, + currentAppId, + location.pathname, + location.search, + navigateToPrefilledEditor, + timeRange, + viewMode, + ]); + + useEffect(() => { + const getCurrentAppId = async () => { + const appId = await currentAppId$.pipe(first()).toPromise(); + setCurrentAppId(appId); + }; + getCurrentAppId(); + }, [currentAppId$]); + + return ( + + {attributes ? ( + <> + + + +
{attributes.title}
+
+
+ + {viewMode && canUseEditor() ? ( + + {`Open visualization`} + + ) : null} + +
+ + + + + + + ) : null} +
+ ); +}; + +export const LensMarkDownRenderer = React.memo(LensMarkDownRendererComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/translations.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/translations.ts new file mode 100644 index 0000000000000..8b09b88136054 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/translations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const INSERT_LENS = i18n.translate( + 'xpack.cases.markdownEditor.plugins.lens.insertLensButtonLabel', + { + defaultMessage: 'Insert visualization', + } +); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts new file mode 100644 index 0000000000000..e615416b2a137 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiMarkdownAstNodePosition } from '@elastic/eui'; +import { useCallback, useEffect, useState } from 'react'; +import { first } from 'rxjs/operators'; +import { useKibana } from '../../../../common/lib/kibana'; +import { DRAFT_COMMENT_STORAGE_ID } from './constants'; +import { INSERT_LENS } from './translations'; + +interface DraftComment { + commentId: string; + comment: string; + position: EuiMarkdownAstNodePosition; + title: string; +} + +export const useLensDraftComment = () => { + const { + application: { currentAppId$ }, + embeddable, + storage, + } = useKibana().services; + const [draftComment, setDraftComment] = useState(null); + + useEffect(() => { + const fetchDraftComment = async () => { + const currentAppId = await currentAppId$.pipe(first()).toPromise(); + + if (!currentAppId) { + return; + } + + const incomingEmbeddablePackage = embeddable + ?.getStateTransfer() + .getIncomingEmbeddablePackage(currentAppId); + + if (incomingEmbeddablePackage) { + if (storage.get(DRAFT_COMMENT_STORAGE_ID)) { + try { + setDraftComment(storage.get(DRAFT_COMMENT_STORAGE_ID)); + // eslint-disable-next-line no-empty + } catch (e) {} + } + } + }; + fetchDraftComment(); + }, [currentAppId$, embeddable, storage]); + + const openLensModal = useCallback(({ editorRef }) => { + if (editorRef && editorRef.textarea && editorRef.toolbar) { + const lensPluginButton = editorRef.toolbar?.querySelector(`[aria-label="${INSERT_LENS}"]`); + if (lensPluginButton) { + lensPluginButton.click(); + } + } + }, []); + + const clearDraftComment = useCallback(() => { + storage.remove(DRAFT_COMMENT_STORAGE_ID); + }, [storage]); + + return { draftComment, openLensModal, clearDraftComment }; +}; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/types.ts b/x-pack/plugins/cases/public/components/markdown_editor/types.ts index ccc3c59c8977e..33249c0025f8e 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/types.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/types.ts @@ -22,7 +22,7 @@ export type TemporaryProcessingPluginsType = [ [ typeof rehype2react, Parameters[0] & { - components: { a: FunctionComponent; timeline: unknown }; + components: { a: FunctionComponent; lens: unknown; timeline: unknown }; } ], ...PluggableList diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts b/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts index e98af8bca8bce..b87b9ae6ad09a 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts @@ -13,8 +13,11 @@ import { import { useMemo } from 'react'; import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { TemporaryProcessingPluginsType } from './types'; +import { KibanaServices } from '../../common/lib/kibana'; +import * as lensMarkdownPlugin from './plugins/lens'; export const usePlugins = () => { + const kibanaConfig = KibanaServices.getConfig(); const timelinePlugins = useTimelineContext()?.editor_plugins; return useMemo(() => { @@ -31,10 +34,18 @@ export const usePlugins = () => { processingPlugins[1][1].components.timeline = timelinePlugins.processingPluginRenderer; } + if (kibanaConfig?.markdownPlugins?.lens) { + uiPlugins.push(lensMarkdownPlugin.plugin); + } + + parsingPlugins.push(lensMarkdownPlugin.parser); + // This line of code is TS-compatible and it will break if [1][1] change in the future. + processingPlugins[1][1].components.lens = lensMarkdownPlugin.renderer; + return { uiPlugins, parsingPlugins, processingPlugins, }; - }, [timelinePlugins]); + }, [kibanaConfig?.markdownPlugins?.lens, timelinePlugins]); }; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/constants.ts b/x-pack/plugins/cases/public/components/user_action_tree/constants.ts new file mode 100644 index 0000000000000..584194be65f50 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_action_tree/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DRAFT_COMMENT_STORAGE_ID = 'xpack.cases.commentDraft'; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 86247b503dff7..b7834585e7423 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -23,7 +23,7 @@ import * as i18n from './translations'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../common/lib/kibana'; -import { AddComment, AddCommentRefObject } from '../add_comment'; +import { AddComment } from '../add_comment'; import { ActionConnector, ActionsCommentRequestRt, @@ -55,6 +55,7 @@ import { UserActionTimestamp } from './user_action_timestamp'; import { UserActionUsername } from './user_action_username'; import { UserActionContentToolbar } from './user_action_content_toolbar'; import { getManualAlertIdsWithNoRuleId } from '../case_view/helpers'; +import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; export interface UserActionTreeProps { caseServices: CaseServices; @@ -155,27 +156,25 @@ export const UserActionTree = React.memo( subCaseId?: string; }>(); const handlerTimeoutId = useRef(0); - const addCommentRef = useRef(null); const [initLoading, setInitLoading] = useState(true); const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState(''); const { isLoadingIds, patchComment } = useUpdateComment(); const currentUser = useCurrentUser(); - const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); + const [manageMarkdownEditIds, setManageMarkdownEditIds] = useState([]); + const commentRefs = useRef>({}); + const { draftComment, openLensModal } = useLensDraftComment(); const [loadingAlertData, manualAlertsData] = useFetchAlertData( getManualAlertIdsWithNoRuleId(caseData.comments) ); - const handleManageMarkdownEditId = useCallback( - (id: string) => { - if (!manageMarkdownEditIds.includes(id)) { - setManangeMardownEditIds([...manageMarkdownEditIds, id]); - } else { - setManangeMardownEditIds(manageMarkdownEditIds.filter((myId) => id !== myId)); - } - }, - [manageMarkdownEditIds] - ); + const handleManageMarkdownEditId = useCallback((id: string) => { + setManageMarkdownEditIds((prevManageMarkdownEditIds) => + !prevManageMarkdownEditIds.includes(id) + ? prevManageMarkdownEditIds.concat(id) + : prevManageMarkdownEditIds.filter((myId) => id !== myId) + ); + }, []); const handleSaveComment = useCallback( ({ id, version }: { id: string; version: string }, content: string) => { @@ -220,8 +219,8 @@ export const UserActionTree = React.memo( (quote: string) => { const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), ' \n> '); - if (addCommentRef && addCommentRef.current) { - addCommentRef.current.addQuote(`> ${addCarrots} \n`); + if (commentRefs.current[NEW_ID]) { + commentRefs.current[NEW_ID].addQuote(`> ${addCarrots} \n`); } handleOutlineComment('add-comment'); @@ -240,6 +239,7 @@ export const UserActionTree = React.memo( const MarkdownDescription = useMemo( () => ( (commentRefs.current[DESCRIPTION_ID] = element)} id={DESCRIPTION_ID} content={caseData.description} isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)} @@ -255,9 +255,10 @@ export const UserActionTree = React.memo( const MarkdownNewComment = useMemo( () => ( (commentRefs.current[NEW_ID] = element)} onCommentPosted={handleUpdate} onCommentSaving={handleManageMarkdownEditId.bind(null, NEW_ID)} showLoading={false} @@ -357,6 +358,7 @@ export const UserActionTree = React.memo( }), children: ( (commentRefs.current[comment.id] = element)} id={comment.id} content={comment.comment} isEditable={manageMarkdownEditIds.includes(comment.id)} @@ -629,6 +631,30 @@ export const UserActionTree = React.memo( const comments = [...userActions, ...bottomActions]; + useEffect(() => { + if (draftComment?.commentId) { + setManageMarkdownEditIds((prevManageMarkdownEditIds) => { + if ( + ![NEW_ID].includes(draftComment?.commentId) && + !prevManageMarkdownEditIds.includes(draftComment?.commentId) + ) { + return [draftComment?.commentId]; + } + return prevManageMarkdownEditIds; + }); + + if ( + commentRefs.current && + commentRefs.current[draftComment.commentId] && + commentRefs.current[draftComment.commentId].editor?.textarea && + commentRefs.current[draftComment.commentId].editor?.toolbar + ) { + commentRefs.current[draftComment.commentId].setComment(draftComment.comment); + openLensModal({ editorRef: commentRefs.current[draftComment.commentId].editor }); + } + } + }, [draftComment, openLensModal]); + return ( <> diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx index cf0d6e3ea50d1..f7a6932b35856 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; import styled from 'styled-components'; import * as i18n from '../case_view/translations'; @@ -25,84 +25,96 @@ interface UserActionMarkdownProps { onChangeEditable: (id: string) => void; onSaveContent: (content: string) => void; } -export const UserActionMarkdown = ({ - id, - content, - isEditable, - onChangeEditable, - onSaveContent, -}: UserActionMarkdownProps) => { - const initialState = { content }; - const { form } = useForm({ - defaultValue: initialState, - options: { stripEmptyFields: false }, - schema, - }); - const fieldName = 'content'; - const { submit } = form; +interface UserActionMarkdownRefObject { + setComment: (newComment: string) => void; +} + +export const UserActionMarkdown = forwardRef( + ({ id, content, isEditable, onChangeEditable, onSaveContent }, ref) => { + const editorRef = useRef(); + const initialState = { content }; + const { form } = useForm({ + defaultValue: initialState, + options: { stripEmptyFields: false }, + schema, + }); + + const fieldName = 'content'; + const { setFieldValue, submit } = form; + + const handleCancelAction = useCallback(() => { + onChangeEditable(id); + }, [id, onChangeEditable]); + + const handleSaveAction = useCallback(async () => { + const { isValid, data } = await submit(); + if (isValid) { + onSaveContent(data.content); + } + onChangeEditable(id); + }, [id, onChangeEditable, onSaveContent, submit]); - const handleCancelAction = useCallback(() => { - onChangeEditable(id); - }, [id, onChangeEditable]); + const setComment = useCallback( + (newComment) => { + setFieldValue(fieldName, newComment); + }, + [setFieldValue] + ); - const handleSaveAction = useCallback(async () => { - const { isValid, data } = await submit(); - if (isValid) { - onSaveContent(data.content); - } - onChangeEditable(id); - }, [id, onChangeEditable, onSaveContent, submit]); + const EditorButtons = useMemo( + () => ( + + + + {i18n.CANCEL} + + + + + {i18n.SAVE} + + + + ), + [handleCancelAction, handleSaveAction] + ); - const renderButtons = useCallback( - ({ cancelAction, saveAction }) => ( - - - - {i18n.CANCEL} - - - - - {i18n.SAVE} - - - - ), - [] - ); + useImperativeHandle(ref, () => ({ + setComment, + editor: editorRef.current, + })); - return isEditable ? ( -
- - - ) : ( - - {content} - - ); -}; + return isEditable ? ( +
+ + + ) : ( + + {content} + + ); + } +); diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 5bfdf9b8b9509..2b4fb40545548 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -17,7 +17,7 @@ import { getRecentCasesLazy, getAllCasesSelectorModalLazy, } from './methods'; -import { ENABLE_CASE_CONNECTOR } from '../common'; +import { CasesUiConfigType, ENABLE_CASE_CONNECTOR } from '../common'; /** * @public @@ -26,7 +26,7 @@ import { ENABLE_CASE_CONNECTOR } from '../common'; export class CasesUiPlugin implements Plugin { private kibanaVersion: string; - constructor(initializerContext: PluginInitializerContext) { + constructor(private readonly initializerContext: PluginInitializerContext) { this.kibanaVersion = initializerContext.env.packageInfo.version; } public setup(core: CoreSetup, plugins: SetupPlugins) { @@ -36,7 +36,8 @@ export class CasesUiPlugin implements Plugin(); + KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion, config }); return { /** * Get the all cases table diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 2b31935c3ff97..db2e5d6ab6bff 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -7,11 +7,17 @@ import { CoreStart } from 'kibana/public'; import { ReactElement } from 'react'; + +import { LensPublicStart } from '../../lens/public'; import { SecurityPluginSetup } from '../../security/public'; -import { +import type { TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup, TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, } from '../../triggers_actions_ui/public'; +import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { Storage } from '../../../../src/plugins/kibana_utils/public'; + import { AllCasesProps } from './components/all_cases'; import { CaseViewProps } from './components/case_view'; import { ConfigureCasesProps } from './components/configure_cases'; @@ -25,6 +31,10 @@ export interface SetupPlugins { } export interface StartPlugins { + data: DataPublicPluginStart; + embeddable: EmbeddableStart; + lens: LensPublicStart; + storage: Storage; triggersActionsUi: TriggersActionsStart; } diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index dd1f09da5cb4a..166ae2ae65012 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -16,6 +16,7 @@ import { Logger, SavedObjectsUtils, } from '../../../../../../src/core/server'; +import { LensServerPluginSetup } from '../../../../lens/server'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { @@ -124,6 +125,7 @@ const addGeneratedAlerts = async ( caseService, userActionService, logger, + lensEmbeddableFactory, authorization, } = clientArgs; @@ -182,6 +184,7 @@ const addGeneratedAlerts = async ( unsecuredSavedObjectsClient, caseService, attachmentService, + lensEmbeddableFactory, }); const { @@ -241,12 +244,14 @@ async function getCombinedCase({ unsecuredSavedObjectsClient, id, logger, + lensEmbeddableFactory, }: { caseService: CasesService; attachmentService: AttachmentService; unsecuredSavedObjectsClient: SavedObjectsClientContract; id: string; logger: Logger; + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; }): Promise { const [casePromise, subCasePromise] = await Promise.allSettled([ caseService.getCase({ @@ -276,6 +281,7 @@ async function getCombinedCase({ caseService, attachmentService, unsecuredSavedObjectsClient, + lensEmbeddableFactory, }); } else { throw Boom.badRequest('Sub case found without reference to collection'); @@ -291,6 +297,7 @@ async function getCombinedCase({ caseService, attachmentService, unsecuredSavedObjectsClient, + lensEmbeddableFactory, }); } } @@ -332,6 +339,7 @@ export const addComment = async ( attachmentService, user, logger, + lensEmbeddableFactory, authorization, } = clientArgs; @@ -362,6 +370,7 @@ export const addComment = async ( unsecuredSavedObjectsClient, id: caseId, logger, + lensEmbeddableFactory, }); // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 157dd0b410898..da505ed55313c 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -9,6 +9,7 @@ import { pick } from 'lodash/fp'; import Boom from '@hapi/boom'; import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { LensServerPluginSetup } from '../../../../lens/server'; import { checkEnabledCaseConnectorOrThrow, CommentableCase, createCaseError } from '../../common'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { @@ -46,6 +47,7 @@ interface CombinedCaseParams { unsecuredSavedObjectsClient: SavedObjectsClientContract; caseID: string; logger: Logger; + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; subCaseId?: string; } @@ -56,6 +58,7 @@ async function getCommentableCase({ caseID, subCaseId, logger, + lensEmbeddableFactory, }: CombinedCaseParams) { if (subCaseId) { const [caseInfo, subCase] = await Promise.all([ @@ -75,6 +78,7 @@ async function getCommentableCase({ subCase, unsecuredSavedObjectsClient, logger, + lensEmbeddableFactory, }); } else { const caseInfo = await caseService.getCase({ @@ -87,6 +91,7 @@ async function getCommentableCase({ collection: caseInfo, unsecuredSavedObjectsClient, logger, + lensEmbeddableFactory, }); } } @@ -105,6 +110,7 @@ export async function update( caseService, unsecuredSavedObjectsClient, logger, + lensEmbeddableFactory, user, userActionService, authorization, @@ -128,6 +134,7 @@ export async function update( caseID, subCaseId: subCaseID, logger, + lensEmbeddableFactory, }); const myComment = await attachmentService.get({ diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 8fcfbe934c3ad..2fae6996f4aa2 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -25,6 +25,8 @@ import { } from '../services'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; +import { LensServerPluginSetup } from '../../../lens/server'; + import { AuthorizationAuditLogger } from '../authorization'; import { CasesClient, createCasesClient } from '.'; @@ -34,6 +36,7 @@ interface CasesClientFactoryArgs { getSpace: GetSpaceFn; featuresPluginStart: FeaturesPluginStart; actionsPluginStart: ActionsPluginStart; + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; } /** @@ -108,6 +111,7 @@ export class CasesClientFactory { userActionService: new CaseUserActionService(this.logger), attachmentService: new AttachmentService(this.logger), logger: this.logger, + lensEmbeddableFactory: this.options.lensEmbeddableFactory, authorization: auth, actionsClient: await this.options.actionsPluginStart.getActionsClientWithRequest(request), }); diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index ebf79519da59a..27829d2539c7d 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -18,6 +18,7 @@ import { AttachmentService, } from '../services'; import { ActionsClient } from '../../../actions/server'; +import { LensServerPluginSetup } from '../../../lens/server'; /** * Parameters for initializing a cases client @@ -33,6 +34,7 @@ export interface CasesClientArgs { readonly alertsService: AlertServiceContract; readonly attachmentService: AttachmentService; readonly logger: Logger; + readonly lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; readonly authorization: PublicMethodsOf; readonly actionsClient: PublicMethodsOf; } diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index 03d6e5b8cea63..856d6378d5900 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -10,9 +10,11 @@ import { SavedObject, SavedObjectReference, SavedObjectsClientContract, + SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, Logger, } from 'src/core/server'; +import { LensServerPluginSetup } from '../../../../lens/server'; import { AssociationType, CASE_SAVED_OBJECT, @@ -29,12 +31,14 @@ import { SUB_CASE_SAVED_OBJECT, SubCaseAttributes, User, + CommentRequestUserType, CaseAttributes, } from '../../../common'; import { flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment } from '..'; import { AttachmentService, CasesService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; +import { getOrUpdateLensReferences } from '../utils'; interface UpdateCommentResp { comment: SavedObjectsUpdateResponse; @@ -53,6 +57,7 @@ interface CommentableCaseParams { caseService: CasesService; attachmentService: AttachmentService; logger: Logger; + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; } /** @@ -66,6 +71,7 @@ export class CommentableCase { private readonly caseService: CasesService; private readonly attachmentService: AttachmentService; private readonly logger: Logger; + private readonly lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; constructor({ collection, @@ -74,6 +80,7 @@ export class CommentableCase { caseService, attachmentService, logger, + lensEmbeddableFactory, }: CommentableCaseParams) { this.collection = collection; this.subCase = subCase; @@ -81,6 +88,7 @@ export class CommentableCase { this.caseService = caseService; this.attachmentService = attachmentService; this.logger = logger; + this.lensEmbeddableFactory = lensEmbeddableFactory; } public get status(): CaseStatuses { @@ -188,6 +196,7 @@ export class CommentableCase { caseService: this.caseService, attachmentService: this.attachmentService, logger: this.logger, + lensEmbeddableFactory: this.lensEmbeddableFactory, }); } catch (error) { throw createCaseError({ @@ -212,6 +221,23 @@ export class CommentableCase { }): Promise { try { const { id, version, ...queryRestAttributes } = updateRequest; + const options: SavedObjectsUpdateOptions = { + version, + }; + + if (queryRestAttributes.type === CommentType.user && queryRestAttributes?.comment) { + const currentComment = (await this.attachmentService.get({ + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, + attachmentId: id, + })) as SavedObject; + + const updatedReferences = getOrUpdateLensReferences( + this.lensEmbeddableFactory, + queryRestAttributes.comment, + currentComment + ); + options.references = updatedReferences; + } const [comment, commentableCase] = await Promise.all([ this.attachmentService.update({ @@ -222,7 +248,7 @@ export class CommentableCase { updated_at: updatedAt, updated_by: user, }, - version, + options, }), this.update({ date: updatedAt, user }), ]); @@ -268,6 +294,16 @@ export class CommentableCase { throw Boom.badRequest('The owner field of the comment must match the case'); } + let references = this.buildRefsToCase(); + + if (commentReq.type === CommentType.user && commentReq?.comment) { + const commentStringReferences = getOrUpdateLensReferences( + this.lensEmbeddableFactory, + commentReq.comment + ); + references = [...references, ...commentStringReferences]; + } + const [comment, commentableCase] = await Promise.all([ this.attachmentService.create({ unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, @@ -277,7 +313,7 @@ export class CommentableCase { ...commentReq, ...user, }), - references: this.buildRefsToCase(), + references, id, }), this.update({ date: createdDate, user }), diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 46ba33a74acd6..e45b91a28ceb3 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -5,13 +5,15 @@ * 2.0. */ -import { SavedObjectsFindResponse } from 'kibana/server'; +import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import { lensEmbeddableFactory } from '../../../lens/server/embeddable/lens_embeddable_factory'; import { SECURITY_SOLUTION_OWNER } from '../../common'; import { AssociationType, CaseResponse, CommentAttributes, CommentRequest, + CommentRequestUserType, CommentType, } from '../../common/api'; import { mockCaseComments, mockCases } from '../routes/api/__fixtures__/mock_saved_objects'; @@ -25,6 +27,8 @@ import { transformComments, flattenCommentSavedObjects, flattenCommentSavedObject, + extractLensReferencesFromCommentString, + getOrUpdateLensReferences, } from './utils'; interface CommentReference { @@ -865,4 +869,130 @@ describe('common utils', () => { ).toEqual(2); }); }); + + describe('extractLensReferencesFromCommentString', () => { + it('extracts successfully', () => { + const commentString = [ + '**Test** ', + 'Amazingg!!!', + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))', + '!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":[{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b246","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b248","name":"indexpattern-datasource-layer-layer1"}]},"editMode":false}}', + '!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":[{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b246","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-layer1"}]},"editMode":false}}', + ].join('\n\n'); + + const extractedReferences = extractLensReferencesFromCommentString( + lensEmbeddableFactory, + commentString + ); + + const expectedReferences = [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b246', + name: 'indexpattern-datasource-current-indexpattern', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b248', + name: 'indexpattern-datasource-layer-layer1', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'indexpattern-datasource-layer-layer1', + }, + ]; + + expect(expectedReferences.length).toEqual(extractedReferences.length); + expect(expectedReferences).toEqual(expect.arrayContaining(extractedReferences)); + }); + }); + + describe('getOrUpdateLensReferences', () => { + it('update references', () => { + const currentCommentStringReferences = [ + [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b246', + name: 'indexpattern-datasource-current-indexpattern', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b248', + name: 'indexpattern-datasource-layer-layer1', + }, + ], + [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b246', + name: 'indexpattern-datasource-current-indexpattern', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b248', + name: 'indexpattern-datasource-layer-layer1', + }, + ], + ]; + const currentCommentString = [ + '**Test** ', + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))', + `!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":${JSON.stringify( + currentCommentStringReferences[0] + )}},"editMode":false}}`, + `!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":${JSON.stringify( + currentCommentStringReferences[1] + )}},"editMode":false}}`, + ].join('\n\n'); + const nonLensCurrentCommentReferences = [ + { type: 'case', id: '7b4be181-9646-41b8-b12d-faabf1bd9512', name: 'Test case' }, + { + type: 'timeline', + id: '0f847d31-9683-4ebd-92b9-454e3e39aec1', + name: 'Test case timeline', + }, + ]; + const currentCommentReferences = [ + ...currentCommentStringReferences.flat(), + ...nonLensCurrentCommentReferences, + ]; + const newCommentStringReferences = [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b245', + name: 'indexpattern-datasource-current-indexpattern', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b248', + name: 'indexpattern-datasource-layer-layer1', + }, + ]; + const newCommentString = [ + '**Test** ', + 'Awmazingg!!!', + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))', + `!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":${JSON.stringify( + newCommentStringReferences + )}},"editMode":false}}`, + ].join('\n\n'); + + const updatedReferences = getOrUpdateLensReferences(lensEmbeddableFactory, newCommentString, { + references: currentCommentReferences, + attributes: { + comment: currentCommentString, + }, + } as SavedObject); + + const expectedReferences = [ + ...nonLensCurrentCommentReferences, + ...newCommentStringReferences, + ]; + + expect(expectedReferences.length).toEqual(updatedReferences.length); + expect(expectedReferences).toEqual(expect.arrayContaining(updatedReferences)); + }); + }); }); diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index bce37764467df..ba7d56f51eea9 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -4,11 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import Boom from '@hapi/boom'; +import unified from 'unified'; +import type { Node, Parent } from 'unist'; +// installed by @elastic/eui +// eslint-disable-next-line import/no-extraneous-dependencies +import markdown from 'remark-parse'; +import remarkStringify from 'remark-stringify'; -import { SavedObjectsFindResult, SavedObjectsFindResponse, SavedObject } from 'kibana/server'; -import { isEmpty } from 'lodash'; +import { + SavedObjectsFindResult, + SavedObjectsFindResponse, + SavedObject, + SavedObjectReference, +} from 'kibana/server'; +import { filter, flatMap, uniqWith, isEmpty, xorWith } from 'lodash'; +import { TimeRange } from 'src/plugins/data/server'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { AlertInfo } from '.'; +import { LensServerPluginSetup, LensDocShape715 } from '../../../lens/server'; import { AssociationType, @@ -33,6 +48,8 @@ import { User, } from '../../common'; import { UpdateAlertRequest } from '../client/alerts/types'; +import { LENS_ID, LensParser, LensSerializer } from '../../common/utils/markdown_plugins/lens'; +import { TimelineSerializer, TimelineParser } from '../../common/utils/markdown_plugins/timeline'; /** * Default sort field for querying saved objects. @@ -398,3 +415,89 @@ export const getNoneCaseConnector = () => ({ type: ConnectorTypes.none, fields: null, }); + +interface LensMarkdownNode extends EmbeddableStateWithType { + timeRange: TimeRange; + attributes: LensDocShape715 & { references: SavedObjectReference[] }; +} + +export const parseCommentString = (comment: string) => { + const processor = unified().use([[markdown, {}], LensParser, TimelineParser]); + return processor.parse(comment) as Parent; +}; + +export const stringifyComment = (comment: Parent) => + unified() + .use([ + [ + remarkStringify, + { + allowDangerousHtml: true, + handlers: { + /* + because we're using rison in the timeline url we need + to make sure that markdown parser doesn't modify the url + */ + timeline: TimelineSerializer, + lens: LensSerializer, + }, + }, + ], + ]) + .stringify(comment); + +export const getLensVisualizations = (parsedComment: Array) => + filter(parsedComment, { type: LENS_ID }) as LensMarkdownNode[]; + +export const extractLensReferencesFromCommentString = ( + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'], + comment: string +): SavedObjectReference[] => { + const extract = lensEmbeddableFactory()?.extract; + + if (extract) { + const parsedComment = parseCommentString(comment); + const lensVisualizations = getLensVisualizations(parsedComment.children); + const flattenRefs = flatMap( + lensVisualizations, + (lensObject) => extract(lensObject)?.references ?? [] + ); + + const uniqRefs = uniqWith( + flattenRefs, + (refA, refB) => refA.type === refB.type && refA.id === refB.id && refA.name === refB.name + ); + + return uniqRefs; + } + return []; +}; + +export const getOrUpdateLensReferences = ( + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'], + newComment: string, + currentComment?: SavedObject +) => { + if (!currentComment) { + return extractLensReferencesFromCommentString(lensEmbeddableFactory, newComment); + } + + const savedObjectReferences = currentComment.references; + const savedObjectLensReferences = extractLensReferencesFromCommentString( + lensEmbeddableFactory, + currentComment.attributes.comment + ); + + const currentNonLensReferences = xorWith( + savedObjectReferences, + savedObjectLensReferences, + (refA, refB) => refA.type === refB.type && refA.id === refB.id + ); + + const newCommentLensReferences = extractLensReferencesFromCommentString( + lensEmbeddableFactory, + newComment + ); + + return currentNonLensReferences.concat(newCommentLensReferences); +}; diff --git a/x-pack/plugins/cases/server/config.ts b/x-pack/plugins/cases/server/config.ts index 7679a5a389051..317f15283e112 100644 --- a/x-pack/plugins/cases/server/config.ts +++ b/x-pack/plugins/cases/server/config.ts @@ -9,6 +9,9 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + markdownPlugins: schema.object({ + lens: schema.boolean({ defaultValue: false }), + }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index 4526ecce28460..5e433b46b80e5 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -12,6 +12,9 @@ import { CasePlugin } from './plugin'; export const config: PluginConfigDescriptor = { schema: ConfigSchema, + exposeToBrowser: { + markdownPlugins: true, + }, deprecations: ({ renameFromRoot }) => [ renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled'), ], diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index b1e2f61a595ee..bb1be163585a8 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -18,7 +18,7 @@ import { APP_ID, ENABLE_CASE_CONNECTOR } from '../common'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; import { - caseCommentSavedObjectType, + createCaseCommentSavedObjectType, caseConfigureSavedObjectType, caseConnectorMappingsSavedObjectType, caseSavedObjectType, @@ -32,6 +32,7 @@ import type { CasesRequestHandlerContext } from './types'; import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { LensServerPluginSetup } from '../../lens/server'; function createConfig(context: PluginInitializerContext) { return context.config.get(); @@ -40,6 +41,7 @@ function createConfig(context: PluginInitializerContext) { export interface PluginsSetup { security?: SecurityPluginSetup; actions: ActionsPluginSetup; + lens: LensServerPluginSetup; } export interface PluginsStart { @@ -66,6 +68,7 @@ export class CasePlugin { private readonly log: Logger; private clientFactory: CasesClientFactory; private securityPluginSetup?: SecurityPluginSetup; + private lensEmbeddableFactory?: LensServerPluginSetup['lensEmbeddableFactory']; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get(); @@ -80,8 +83,15 @@ export class CasePlugin { } this.securityPluginSetup = plugins.security; + this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory; - core.savedObjects.registerType(caseCommentSavedObjectType); + core.savedObjects.registerType( + createCaseCommentSavedObjectType({ + migrationDeps: { + lensEmbeddableFactory: this.lensEmbeddableFactory, + }, + }) + ); core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseConnectorMappingsSavedObjectType); core.savedObjects.registerType(caseSavedObjectType); @@ -127,6 +137,7 @@ export class CasePlugin { }, featuresPluginStart: plugins.features, actionsPluginStart: plugins.actions, + lensEmbeddableFactory: this.lensEmbeddableFactory!, }); const client = core.elasticsearch.client; diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index 876ceb9bc2045..0384a65dcb389 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -7,11 +7,15 @@ import { SavedObjectsType } from 'src/core/server'; import { CASE_COMMENT_SAVED_OBJECT } from '../../common'; -import { commentsMigrations } from './migrations'; +import { createCommentsMigrations, CreateCommentsMigrationsDeps } from './migrations'; -export const caseCommentSavedObjectType: SavedObjectsType = { +export const createCaseCommentSavedObjectType = ({ + migrationDeps, +}: { + migrationDeps: CreateCommentsMigrationsDeps; +}): SavedObjectsType => ({ name: CASE_COMMENT_SAVED_OBJECT, - hidden: true, + hidden: false, namespaceType: 'single', mappings: { properties: { @@ -105,5 +109,5 @@ export const caseCommentSavedObjectType: SavedObjectsType = { }, }, }, - migrations: commentsMigrations, -}; + migrations: () => createCommentsMigrations(migrationDeps), +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/index.ts b/x-pack/plugins/cases/server/saved_object_types/index.ts index 1c6bcf6ca710a..2c39a10f61da7 100644 --- a/x-pack/plugins/cases/server/saved_object_types/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/index.ts @@ -8,6 +8,6 @@ export { caseSavedObjectType } from './cases'; export { subCaseSavedObjectType } from './sub_case'; export { caseConfigureSavedObjectType } from './configure'; -export { caseCommentSavedObjectType } from './comments'; +export { createCaseCommentSavedObjectType } from './comments'; export { caseUserActionSavedObjectType } from './user_actions'; export { caseConnectorMappingsSavedObjectType } from './connector_mappings'; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts new file mode 100644 index 0000000000000..595ecf290c520 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts @@ -0,0 +1,236 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createCommentsMigrations } from './index'; +import { getLensVisualizations, parseCommentString } from '../../common'; + +import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks'; +import { lensEmbeddableFactory } from '../../../../lens/server/embeddable/lens_embeddable_factory'; + +const migrations = createCommentsMigrations({ + lensEmbeddableFactory, +}); + +const contextMock = savedObjectsServiceMock.createMigrationContext(); + +describe('lens embeddable migrations for by value panels', () => { + describe('7.14.0 remove time zone from Lens visualization date histogram', () => { + const lensVisualizationToMigrate = { + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + datasourceStates: { + indexpattern: { + layers: { + '2': { + columns: { + '3': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto', timeZone: 'Europe/Berlin' }, + }, + '4': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5': { + label: '@timestamp', + dataType: 'date', + operationType: 'my_unexpected_operation', + isBucketed: true, + scale: 'interval', + params: { timeZone: 'do not delete' }, + }, + }, + columnOrder: ['3', '4', '5'], + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + title: 'Empty XY chart', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', + accessors: [ + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', + ], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }; + + const expectedLensVisualizationMigrated = { + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + datasourceStates: { + indexpattern: { + layers: { + '2': { + columns: { + '3': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '4': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5': { + label: '@timestamp', + dataType: 'date', + operationType: 'my_unexpected_operation', + isBucketed: true, + scale: 'interval', + params: { timeZone: 'do not delete' }, + }, + }, + columnOrder: ['3', '4', '5'], + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + title: 'Empty XY chart', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', + accessors: [ + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', + ], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }; + + const expectedMigrationCommentResult = `"**Amazing**\n\n!{tooltip[Tessss](https://example.com)}\n\nbrbrbr\n\n[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"attributes\":${JSON.stringify( + expectedLensVisualizationMigrated + )}}}\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"attributes\":{\"title\":\"TEst22\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"layer1\":{\"columnOrder\":[\"col2\"],\"columns\":{\"col2\":{\"dataType\":\"number\",\"isBucketed\":false,\"label\":\"Count of records\",\"operationType\":\"count\",\"scale\":\"ratio\",\"sourceField\":\"Records\"}}}}}},\"visualization\":{\"layerId\":\"layer1\",\"accessor\":\"col2\"},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-layer-layer1\"}]}}}\n\nbrbrbr" +`; + + const caseComment = { + type: 'cases-comments', + id: '1cefd0d0-e86d-11eb-bae5-3d065cd16a32', + attributes: { + associationType: 'case', + comment: `"**Amazing**\n\n!{tooltip[Tessss](https://example.com)}\n\nbrbrbr\n\n[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":${JSON.stringify( + lensVisualizationToMigrate + )}}}\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":{\"title\":\"TEst22\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"layer1\":{\"columnOrder\":[\"col2\"],\"columns\":{\"col2\":{\"dataType\":\"number\",\"isBucketed\":false,\"label\":\"Count of records\",\"operationType\":\"count\",\"scale\":\"ratio\",\"sourceField\":\"Records\"}}}}}},\"visualization\":{\"layerId\":\"layer1\",\"accessor\":\"col2\"},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-layer-layer1\"}]}}}\n\nbrbrbr"`, + type: 'user', + created_at: '2021-07-19T08:41:29.951Z', + created_by: { + email: null, + full_name: null, + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2021-07-19T08:41:47.549Z', + updated_by: { + full_name: null, + email: null, + username: 'elastic', + }, + }, + references: [ + { + name: 'associated-cases', + id: '77d1b230-d35e-11eb-8da6-6f746b9cb499', + type: 'cases', + }, + { + name: 'indexpattern-datasource-current-indexpattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'index-pattern', + }, + { + name: 'indexpattern-datasource-current-indexpattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'index-pattern', + }, + ], + migrationVersion: { + 'cases-comments': '7.14.0', + }, + coreMigrationVersion: '8.0.0', + updated_at: '2021-07-19T08:41:47.552Z', + version: 'WzgxMTY4MSw5XQ==', + namespaces: ['default'], + score: 0, + }; + + it('should remove time zone param from date histogram', () => { + expect(migrations['7.14.0']).toBeDefined(); + const result = migrations['7.14.0'](caseComment, contextMock); + + const parsedComment = parseCommentString(result.attributes.comment); + const lensVisualizations = getLensVisualizations(parsedComment.children); + + const layers = Object.values( + lensVisualizations[0].attributes.state.datasourceStates.indexpattern.layers + ); + expect(result.attributes.comment).toEqual(expectedMigrationCommentResult); + expect(layers.length).toBe(1); + const columns = Object.values(layers[0].columns); + expect(columns.length).toBe(3); + expect(columns[0].operationType).toEqual('date_histogram'); + expect((columns[0] as { params: {} }).params).toEqual({ interval: 'auto' }); + expect(columns[1].operationType).toEqual('date_histogram'); + expect((columns[1] as { params: {} }).params).toEqual({ interval: 'auto' }); + expect(columns[2].operationType).toEqual('my_unexpected_operation'); + expect((columns[2] as { params: {} }).params).toEqual({ timeZone: 'do not delete' }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts index 7be87c3abc989..b1792d98cfdb2 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts @@ -7,9 +7,19 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { flow, mapValues } from 'lodash'; +import { LensServerPluginSetup } from '../../../../lens/server'; + +import { + mergeMigrationFunctionMaps, + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../../src/plugins/kibana_utils/common'; import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, + SavedObjectMigrationFn, + SavedObjectMigrationMap, } from '../../../../../../src/core/server'; import { ConnectorTypes, @@ -17,6 +27,7 @@ import { AssociationType, SECURITY_SOLUTION_OWNER, } from '../../../common'; +import { parseCommentString, stringifyComment } from '../../common'; export { caseMigrations } from './cases'; export { configureMigrations } from './configuration'; @@ -103,44 +114,86 @@ interface SanitizedCommentForSubCases { rule?: { id: string | null; name: string | null }; } -export const commentsMigrations = { - '7.11.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - type: CommentType.user, - }, - references: doc.references || [], - }; - }, - '7.12.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { - ...doc.attributes, - associationType: AssociationType.case, - }; - - // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are - // introduced in 7.12. - if (doc.attributes.type === CommentType.alert) { - attributes = { ...attributes, rule: { id: null, name: null } }; +const migrateByValueLensVisualizations = ( + migrate: MigrateFunction, + version: string +): SavedObjectMigrationFn => (doc: any) => { + const parsedComment = parseCommentString(doc.attributes.comment); + const migratedComment = parsedComment.children.map((comment) => { + if (comment?.type === 'lens') { + // @ts-expect-error + return migrate(comment); } - return { - ...doc, - attributes, - references: doc.references || [], - }; - }, - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, + return comment; + }); + + // @ts-expect-error + parsedComment.children = migratedComment; + doc.attributes.comment = stringifyComment(parsedComment); + + return doc; +}; + +export interface CreateCommentsMigrationsDeps { + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; +} + +export const createCommentsMigrations = ( + migrationDeps: CreateCommentsMigrationsDeps +): SavedObjectMigrationMap => { + const embeddableMigrations = mapValues( + migrationDeps.lensEmbeddableFactory().migrations, + migrateByValueLensVisualizations + ) as MigrateFunctionsObject; + + const commentsMigrations = { + '7.11.0': flow( + ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + type: CommentType.user, + }, + references: doc.references || [], + }; + } + ), + '7.12.0': flow( + ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { + ...doc.attributes, + associationType: AssociationType.case, + }; + + // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are + // introduced in 7.12. + if (doc.attributes.type === CommentType.alert) { + attributes = { ...attributes, rule: { id: null, name: null } }; + } + + return { + ...doc, + attributes, + references: doc.references || [], + }; + } + ), + '7.14.0': flow( + ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + } + ), + }; + + return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations); }; export const connectorMappingsMigrations = { diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index c2d9b4826fc14..105b6a3125523 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { Logger, SavedObject, SavedObjectReference } from 'kibana/server'; +import { + Logger, + SavedObject, + SavedObjectReference, + SavedObjectsUpdateOptions, +} from 'kibana/server'; import { KueryNode } from '../../../../../../src/plugins/data/common'; import { @@ -38,10 +43,10 @@ interface CreateAttachmentArgs extends ClientArgs { interface UpdateArgs { attachmentId: string; updatedAttributes: AttachmentPatchAttributes; - version?: string; + options?: SavedObjectsUpdateOptions; } -type UpdateAttachmentArgs = UpdateArgs & ClientArgs; +export type UpdateAttachmentArgs = UpdateArgs & ClientArgs; interface BulkUpdateAttachmentArgs extends ClientArgs { comments: UpdateArgs[]; @@ -142,7 +147,7 @@ export class AttachmentService { unsecuredSavedObjectsClient, attachmentId, updatedAttributes, - version, + options, }: UpdateAttachmentArgs) { try { this.log.debug(`Attempting to UPDATE comment ${attachmentId}`); @@ -150,7 +155,7 @@ export class AttachmentService { CASE_COMMENT_SAVED_OBJECT, attachmentId, updatedAttributes, - { version } + options ); } catch (error) { this.log.error(`Error on UPDATE comment ${attachmentId}: ${error}`); @@ -168,7 +173,7 @@ export class AttachmentService { type: CASE_COMMENT_SAVED_OBJECT, id: c.attachmentId, attributes: c.updatedAttributes, - version: c.version, + ...c.options, })) ); } catch (error) { diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index 99622df805ced..1c9373e023366 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -16,6 +16,7 @@ { "path": "../../../src/core/tsconfig.json" }, // optionalPlugins from ./kibana.json + { "path": "../lens/tsconfig.json" }, { "path": "../security/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, @@ -24,6 +25,7 @@ { "path": "../triggers_actions_ui/tsconfig.json"}, { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" } + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json" } ] } diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json index f130d0173cc89..0a594cf1cc2ac 100644 --- a/x-pack/plugins/cross_cluster_replication/kibana.json +++ b/x-pack/plugins/cross_cluster_replication/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": [ "home", "licensing", @@ -12,13 +16,7 @@ "indexManagement", "features" ], - "optionalPlugins": [ - "usageCollection" - ], + "optionalPlugins": ["usageCollection"], "configPath": ["xpack", "ccr"], - "requiredBundles": [ - "kibanaReact", - "esUiShared", - "data" - ] + "requiredBundles": ["kibanaReact", "esUiShared", "data"] } diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index da83ded471d0b..d678921e9ac7b 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -3,6 +3,10 @@ "id": "dataEnhanced", "version": "8.0.0", "kibanaVersion": "kibana", + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "configPath": ["xpack", "data_enhanced"], "requiredPlugins": ["bfetch", "data", "features", "management", "share", "taskManager"], "optionalPlugins": ["kibanaUtils", "usageCollection", "security"], diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_data_viz_chart_theme.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_data_viz_chart_theme.ts index ad31ca2d09420..56b5049786aca 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_data_viz_chart_theme.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_data_viz_chart_theme.ts @@ -10,7 +10,7 @@ import { useMemo } from 'react'; import { useCurrentEuiTheme } from './use_color_range'; export const useDataVizChartTheme = (): PartialTheme => { const { euiTheme } = useCurrentEuiTheme(); - const chartTheme = useMemo(() => { + const chartTheme = useMemo(() => { const AREA_SERIES_COLOR = euiTheme.euiColorVis0; return { axes: { diff --git a/x-pack/plugins/drilldowns/url_drilldown/kibana.json b/x-pack/plugins/drilldowns/url_drilldown/kibana.json index 9bdd13fbfea26..a4552d201f263 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/kibana.json +++ b/x-pack/plugins/drilldowns/url_drilldown/kibana.json @@ -3,6 +3,10 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["embeddable", "uiActions", "uiActionsEnhanced"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index 8d49e3e26eb7b..09416ce18aecb 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,5 +3,9 @@ "version": "kibana", "server": false, "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"] } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx index 80c72235f7a4a..c9a540b9bf72b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx @@ -43,6 +43,9 @@ const values: { domains: CrawlerDomain[]; crawlRequests: CrawlRequest[] } = { rule: CrawlerRules.regex, pattern: '.*', }, + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }, ], crawlRequests: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.scss new file mode 100644 index 0000000000000..6190a0beb91bc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.scss @@ -0,0 +1,14 @@ +.deduplicationPanel { + .selectableWrapper { + padding: $euiSize; + border-radius: $euiSize *.675; + border: $euiBorderThin solid $euiColorLightestShade; + } + + .showAllFieldsPopoverToggle { + .euiButtonEmpty__content { + padding-left: $euiSizeM; + padding-right: $euiSizeM; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx new file mode 100644 index 0000000000000..9c076c5550a34 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { act } from 'react-dom/test-utils'; + +import { + EuiButton, + EuiButtonEmpty, + EuiContextMenuItem, + EuiPopover, + EuiSelectable, + EuiSelectableList, + EuiSelectableSearch, + EuiSwitch, +} from '@elastic/eui'; + +import { mountWithIntl, rerender } from '../../../../../test_helpers'; + +import { DataPanel } from '../../../data_panel'; + +import { DeduplicationPanel } from './deduplication_panel'; + +const MOCK_ACTIONS = { + submitDeduplicationUpdate: jest.fn(), +}; + +const MOCK_VALUES = { + domain: { + deduplicationEnabled: true, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], + }, +}; + +describe('DeduplicationPanel', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + setMockValues(MOCK_VALUES); + }); + + it('renders an empty component if no domain', () => { + setMockValues({ + ...MOCK_VALUES, + domain: null, + }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('contains a button to reset to defaults', () => { + const wrapper = shallow(); + + wrapper.find(DataPanel).dive().find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.submitDeduplicationUpdate).toHaveBeenCalledWith(MOCK_VALUES.domain, { + fields: [], + }); + }); + + it('contains a switch to enable and disable deduplication', () => { + setMockValues({ + ...MOCK_VALUES, + domain: { + ...MOCK_VALUES.domain, + deduplicationEnabled: false, + }, + }); + const wrapper = shallow(); + + wrapper.find(EuiSwitch).simulate('change'); + + expect(MOCK_ACTIONS.submitDeduplicationUpdate).toHaveBeenNthCalledWith( + 1, + { + ...MOCK_VALUES.domain, + deduplicationEnabled: false, + }, + { + enabled: true, + } + ); + + setMockValues({ + ...MOCK_VALUES, + domain: { + ...MOCK_VALUES.domain, + deduplicationEnabled: true, + }, + }); + rerender(wrapper); + + wrapper.find(EuiSwitch).simulate('change'); + + expect(MOCK_ACTIONS.submitDeduplicationUpdate).toHaveBeenNthCalledWith( + 2, + { + ...MOCK_VALUES.domain, + deduplicationEnabled: true, + }, + { + enabled: false, + fields: [], + } + ); + }); + + it('contains a popover to switch between displaying all fields or only selected ones', () => { + const fullRender = mountWithIntl(); + + expect(fullRender.find(EuiButtonEmpty).text()).toEqual('All fields'); + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(false); + + // Open the popover + fullRender.find(EuiButtonEmpty).simulate('click'); + rerender(fullRender); + + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(true); + + // Click "Show selected fields" + fullRender.find(EuiContextMenuItem).at(1).simulate('click'); + rerender(fullRender); + + expect(fullRender.find(EuiButtonEmpty).text()).toEqual('Selected fields'); + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(false); + + // Open the popover and click "show all fields" + fullRender.find(EuiButtonEmpty).simulate('click'); + fullRender.find(EuiContextMenuItem).at(0).simulate('click'); + rerender(fullRender); + + expect(fullRender.find(EuiButtonEmpty).text()).toEqual('All fields'); + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(false); + + // Open the popover then simulate closing the popover + fullRender.find(EuiButtonEmpty).simulate('click'); + act(() => { + fullRender.find(EuiPopover).prop('closePopover')(); + }); + rerender(fullRender); + + expect(fullRender.find(EuiPopover).prop('isOpen')).toEqual(false); + }); + + it('contains a selectable to toggle fields for deduplication', () => { + const wrapper = shallow(); + + wrapper + .find(EuiSelectable) + .simulate('change', [{ label: 'title' }, { label: 'description', checked: 'on' }]); + + expect(MOCK_ACTIONS.submitDeduplicationUpdate).toHaveBeenCalledWith(MOCK_VALUES.domain, { + fields: ['description'], + }); + + const fullRender = mountWithIntl(); + + expect(fullRender.find(EuiSelectableSearch)).toHaveLength(1); + expect(fullRender.find(EuiSelectableList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx new file mode 100644 index 0000000000000..a25583f91763e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPopover, + EuiSelectable, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; + +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { DOCS_PREFIX } from '../../../../routes'; +import { DataPanel } from '../../../data_panel'; +import { CrawlerSingleDomainLogic } from '../../crawler_single_domain_logic'; + +import { getCheckedOptionLabels, getSelectableOptions } from './utils'; + +import './deduplication_panel.scss'; + +export const DeduplicationPanel: React.FC = () => { + const { domain } = useValues(CrawlerSingleDomainLogic); + const { submitDeduplicationUpdate } = useActions(CrawlerSingleDomainLogic); + + const [showAllFields, setShowAllFields] = useState(true); + const [showAllFieldsPopover, setShowAllFieldsPopover] = useState(false); + + if (!domain) { + return null; + } + + const { deduplicationEnabled, deduplicationFields } = domain; + + const selectableOptions = getSelectableOptions(domain, showAllFields); + + return ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.title', { + defaultMessage: 'Duplicate document handling', + })} + + } + action={ + submitDeduplicationUpdate(domain, { fields: [] })} + disabled={deduplicationFields.length === 0} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.resetToDefaultsButtonLabel', + { + defaultMessage: 'Reset to defaults', + } + )} + + } + subtitle={ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.learnMoreMessage', + { + defaultMessage: 'Learn more about content hashing', + } + )} + + ), + }} + /> + } + > + + deduplicationEnabled + ? submitDeduplicationUpdate(domain, { enabled: false, fields: [] }) + : submitDeduplicationUpdate(domain, { enabled: true }) + } + /> + + + +
+ + submitDeduplicationUpdate(domain, { + fields: getCheckedOptionLabels(options as Array>), + }) + } + searchable + searchProps={{ + disabled: !deduplicationEnabled, + append: ( + setShowAllFieldsPopover(!showAllFieldsPopover)} + className="showAllFieldsPopoverToggle" + disabled={!deduplicationEnabled} + > + {showAllFields + ? i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.allFieldsLabel', + { + defaultMessage: 'All fields', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.selectedFieldsLabel', + { + defaultMessage: 'Selected fields', + } + )} + + } + isOpen={showAllFieldsPopover} + closePopover={() => setShowAllFieldsPopover(false)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + setShowAllFields(true); + setShowAllFieldsPopover(false); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.showAllFieldsButtonLabel', + { + defaultMessage: 'Show all fields', + } + )} + , + { + setShowAllFields(false); + setShowAllFieldsPopover(false); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.showSelectedFieldsButtonLabel', + { + defaultMessage: 'Show only selected fields', + } + )} + , + ]} + /> + + ), + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/index.ts new file mode 100644 index 0000000000000..23545e91a7a69 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { DeduplicationPanel } from './deduplication_panel'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.test.ts new file mode 100644 index 0000000000000..58d8e1effa159 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; + +import { CrawlerDomain } from '../../types'; + +import { getCheckedOptionLabels, getSelectableOptions } from './utils'; + +describe('getCheckedOptionLabels', () => { + it('returns the labels of selected options', () => { + const options = [{ label: 'title' }, { label: 'description', checked: 'on' }] as Array< + EuiSelectableLIOption + >; + + expect(getCheckedOptionLabels(options)).toEqual(['description']); + }); +}); + +describe('getSelectableOptions', () => { + it('returns all available fields when we want all fields', () => { + expect( + getSelectableOptions( + { + availableDeduplicationFields: ['title', 'description'], + deduplicationFields: ['title'], + deduplicationEnabled: true, + } as CrawlerDomain, + true + ) + ).toEqual([ + { label: 'title', checked: 'on' }, + { label: 'description', checked: undefined }, + ]); + }); + + it('can returns only selected fields', () => { + expect( + getSelectableOptions( + { + availableDeduplicationFields: ['title', 'description'], + deduplicationFields: ['title'], + deduplicationEnabled: true, + } as CrawlerDomain, + false + ) + ).toEqual([{ label: 'title', checked: 'on' }]); + }); + + it('disables all options when deduplication is disabled', () => { + expect( + getSelectableOptions( + { + availableDeduplicationFields: ['title', 'description'], + deduplicationFields: ['title'], + deduplicationEnabled: false, + } as CrawlerDomain, + true + ) + ).toEqual([ + { label: 'title', checked: 'on', disabled: true }, + { label: 'description', checked: undefined, disabled: true }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.ts new file mode 100644 index 0000000000000..f0ef7ece0c6a6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/utils.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; + +import { CrawlerDomain } from '../../types'; + +export const getSelectableOptions = ( + domain: CrawlerDomain, + showAllFields: boolean +): Array> => { + const { availableDeduplicationFields, deduplicationFields, deduplicationEnabled } = domain; + + let selectableOptions: Array>; + + if (showAllFields) { + selectableOptions = availableDeduplicationFields.map((field) => ({ + label: field, + checked: deduplicationFields.includes(field) ? 'on' : undefined, + })); + } else { + selectableOptions = availableDeduplicationFields + .filter((field) => deduplicationFields.includes(field)) + .map((field) => ({ label: field, checked: 'on' })); + } + + if (!deduplicationEnabled) { + selectableOptions = selectableOptions.map((option) => ({ ...option, disabled: true })); + } + + return selectableOptions; +}; + +export const getCheckedOptionLabels = (options: Array>): string[] => { + return options.filter((option) => option.checked).map((option) => option.label); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx index 084d9693fe279..6b8377775021c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -27,6 +27,14 @@ export const DeleteDomainPanel: React.FC = ({}) => { return ( <> + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.deleteDomainPanel.title', { + defaultMessage: 'Delete domain', + })} +

+
+

{ @@ -23,7 +25,7 @@ describe('EntryPointsTable', () => { { id: '1', value: '/whatever' }, { id: '2', value: '/foo' }, ]; - const domain = { + const domain: CrawlerDomain = { createdOn: '2018-01-01T00:00:00.000Z', documentCount: 10, id: '6113e1407a2f2e6f42489794', @@ -31,6 +33,9 @@ describe('EntryPointsTable', () => { crawlRules: [], entryPoints, sitemaps: [], + deduplicationEnabled: true, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }; beforeEach(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx index 8d7aa83cd2ec6..8bfc5cdc45e4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/sitemaps_table.test.tsx @@ -34,6 +34,9 @@ describe('SitemapsTable', () => { crawlRules: [], entryPoints: [], sitemaps, + deduplicationEnabled: true, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }; beforeEach(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 8e8ed0d4c9258..97c7a3e47ae59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -47,6 +47,9 @@ const domains: CrawlerDomainFromServer[] = [ rule: CrawlerRules.regex, pattern: '.*', }, + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, { id: 'y', @@ -57,6 +60,9 @@ const domains: CrawlerDomainFromServer[] = [ sitemaps: [], entry_points: [], crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts index 86f6e14631329..97a050152a543 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts @@ -52,6 +52,9 @@ const MOCK_SERVER_CRAWLER_DATA: CrawlerDataFromServer = { sitemaps: [], entry_points: [], crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, ], }; @@ -112,6 +115,9 @@ describe('CrawlerOverviewLogic', () => { entryPoints: [], crawlRules: [], defaultCrawlRule: DEFAULT_CRAWL_RULE, + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }, ], }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index 903068e28c39a..76612ee913c48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -13,15 +13,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiCode } from '@elastic/eui'; - import { getPageHeaderActions } from '../../../test_helpers'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; +import { DeduplicationPanel } from './components/deduplication_panel'; import { DeleteDomainPanel } from './components/delete_domain_panel'; import { ManageCrawlsPopover } from './components/manage_crawls_popover/manage_crawls_popover'; -import { CrawlerOverview } from './crawler_overview'; import { CrawlerSingleDomain } from './crawler_single_domain'; const MOCK_VALUES = { @@ -53,7 +51,6 @@ describe('CrawlerSingleDomain', () => { const wrapper = shallow(); expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1); - expect(wrapper.find(EuiCode).render().text()).toContain('https://elastic.co'); expect(wrapper.prop('pageHeader').pageTitle).toEqual('https://elastic.co'); }); @@ -71,20 +68,32 @@ describe('CrawlerSingleDomain', () => { }); it('contains a crawler status banner', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(CrawlerStatusBanner)).toHaveLength(1); }); it('contains a crawler status indicator', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(getPageHeaderActions(wrapper).find(CrawlerStatusIndicator)).toHaveLength(1); }); it('contains a popover to manage crawls', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(getPageHeaderActions(wrapper).find(ManageCrawlsPopover)).toHaveLength(1); }); + + it('contains a panel to manage deduplication settings', () => { + const wrapper = shallow(); + + expect(wrapper.find(DeduplicationPanel)).toHaveLength(1); + }); + + it('contains a panel to delete the domain', () => { + const wrapper = shallow(); + + expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index b93fb8592cff8..a4b2a9709cd62 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -11,9 +11,7 @@ import { useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { EuiCode, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; @@ -21,6 +19,7 @@ import { AppSearchPageTemplate } from '../layout'; import { CrawlRulesTable } from './components/crawl_rules_table'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; +import { DeduplicationPanel } from './components/deduplication_panel'; import { DeleteDomainPanel } from './components/delete_domain_panel'; import { EntryPointsTable } from './components/entry_points_table'; import { ManageCrawlsPopover } from './components/manage_crawls_popover/manage_crawls_popover'; @@ -76,20 +75,9 @@ export const CrawlerSingleDomain: React.FC = () => { )} - -

- {i18n.translate( - 'xpack.enterpriseSearch.appSearch.crawler.singleDomain.deleteDomainTitle', - { - defaultMessage: 'Delete domain', - } - )} -

- - - + - {JSON.stringify(domain, null, 2)} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts index 492bd363a5f2d..bf0add6df5cfe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts @@ -216,5 +216,62 @@ describe('CrawlerSingleDomainLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); + + describe('submitDeduplicationUpdate', () => { + it('updates logic with data that has been converted from server to client', async () => { + jest.spyOn(CrawlerSingleDomainLogic.actions, 'onReceiveDomainData'); + http.put.mockReturnValueOnce( + Promise.resolve({ + id: '507f1f77bcf86cd799439011', + name: 'https://elastic.co', + created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', + document_count: 13, + sitemaps: [], + entry_points: [], + crawl_rules: [], + deduplication_enabled: true, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], + }) + ); + + CrawlerSingleDomainLogic.actions.submitDeduplicationUpdate( + { id: '507f1f77bcf86cd799439011' } as CrawlerDomain, + { fields: ['title'], enabled: true } + ); + await nextTick(); + + expect(http.put).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/crawler/domains/507f1f77bcf86cd799439011', + { + body: JSON.stringify({ deduplication_enabled: true, deduplication_fields: ['title'] }), + } + ); + expect(CrawlerSingleDomainLogic.actions.onReceiveDomainData).toHaveBeenCalledWith({ + id: '507f1f77bcf86cd799439011', + createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', + url: 'https://elastic.co', + documentCount: 13, + sitemaps: [], + entryPoints: [], + crawlRules: [], + deduplicationEnabled: true, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], + }); + }); + + it('displays any errors to the user', async () => { + http.put.mockReturnValueOnce(Promise.reject('error')); + + CrawlerSingleDomainLogic.actions.submitDeduplicationUpdate( + { id: '507f1f77bcf86cd799439011' } as CrawlerDomain, + { fields: ['title'], enabled: true } + ); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts index 78912f736926d..e9c74c864b1b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts @@ -29,6 +29,10 @@ interface CrawlerSingleDomainActions { updateCrawlRules(crawlRules: CrawlRule[]): { crawlRules: CrawlRule[] }; updateEntryPoints(entryPoints: EntryPoint[]): { entryPoints: EntryPoint[] }; updateSitemaps(entryPoints: Sitemap[]): { sitemaps: Sitemap[] }; + submitDeduplicationUpdate( + domain: CrawlerDomain, + payload: { fields?: string[]; enabled?: boolean } + ): { domain: CrawlerDomain; fields: string[]; enabled: boolean }; } export const CrawlerSingleDomainLogic = kea< @@ -42,6 +46,7 @@ export const CrawlerSingleDomainLogic = kea< updateCrawlRules: (crawlRules) => ({ crawlRules }), updateEntryPoints: (entryPoints) => ({ entryPoints }), updateSitemaps: (sitemaps) => ({ sitemaps }), + submitDeduplicationUpdate: (domain, { fields, enabled }) => ({ domain, fields, enabled }), }, reducers: { dataLoading: [ @@ -88,6 +93,30 @@ export const CrawlerSingleDomainLogic = kea< const domainData = crawlerDomainServerToClient(response); + actions.onReceiveDomainData(domainData); + } catch (e) { + flashAPIErrors(e); + } + }, + submitDeduplicationUpdate: async ({ domain, fields, enabled }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const payload = { + deduplication_enabled: enabled, + deduplication_fields: fields, + }; + + try { + const response = await http.put( + `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}`, + { + body: JSON.stringify(payload), + } + ); + + const domainData = crawlerDomainServerToClient(response); + actions.onReceiveDomainData(domainData); } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts index 1b46e21dbcb72..932af7a6ac93b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -98,6 +98,9 @@ export interface CrawlerDomain { defaultCrawlRule?: CrawlRule; entryPoints: EntryPoint[]; sitemaps: Sitemap[]; + deduplicationEnabled: boolean; + deduplicationFields: string[]; + availableDeduplicationFields: string[]; } export interface CrawlerDomainFromServer { @@ -110,6 +113,9 @@ export interface CrawlerDomainFromServer { default_crawl_rule?: CrawlRule; entry_points: EntryPoint[]; sitemaps: Sitemap[]; + deduplication_enabled: boolean; + deduplication_fields: string[]; + available_deduplication_fields: string[]; } export interface CrawlerData { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts index e356fae46f30e..1844932bac926 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts @@ -16,6 +16,7 @@ import { CrawlerStatus, CrawlerData, CrawlRequest, + CrawlerDomain, } from './types'; import { @@ -39,7 +40,7 @@ describe('crawlerDomainServerToClient', () => { const id = '507f1f77bcf86cd799439011'; const name = 'moviedatabase.com'; - const defaultServerPayload = { + const defaultServerPayload: CrawlerDomainFromServer = { id, name, created_on: 'Mon, 31 Aug 2020 17:00:00 +0000', @@ -47,9 +48,12 @@ describe('crawlerDomainServerToClient', () => { sitemaps: [], entry_points: [], crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }; - const defaultClientPayload = { + const defaultClientPayload: CrawlerDomain = { id, createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000', url: name, @@ -57,6 +61,9 @@ describe('crawlerDomainServerToClient', () => { sitemaps: [], entryPoints: [], crawlRules: [], + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }; expect(crawlerDomainServerToClient(defaultServerPayload)).toStrictEqual(defaultClientPayload); @@ -124,6 +131,9 @@ describe('crawlerDataServerToClient', () => { entry_points: [], crawl_rules: [], default_crawl_rule: DEFAULT_CRAWL_RULE, + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, { id: 'y', @@ -134,6 +144,9 @@ describe('crawlerDataServerToClient', () => { sitemaps: [], entry_points: [], crawl_rules: [], + deduplication_enabled: false, + deduplication_fields: ['title'], + available_deduplication_fields: ['title', 'description'], }, ]; @@ -154,6 +167,9 @@ describe('crawlerDataServerToClient', () => { entryPoints: [], crawlRules: [], defaultCrawlRule: DEFAULT_CRAWL_RULE, + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }, { id: 'y', @@ -164,6 +180,9 @@ describe('crawlerDataServerToClient', () => { sitemaps: [], entryPoints: [], crawlRules: [], + deduplicationEnabled: false, + deduplicationFields: ['title'], + availableDeduplicationFields: ['title', 'description'], }, ]); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts index a25025dc08522..1f54db12a0217 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts @@ -29,6 +29,9 @@ export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): C crawl_rules: crawlRules, default_crawl_rule: defaultCrawlRule, entry_points: entryPoints, + deduplication_enabled: deduplicationEnabled, + deduplication_fields: deduplicationFields, + available_deduplication_fields: availableDeduplicationFields, } = payload; const clientPayload: CrawlerDomain = { @@ -39,6 +42,9 @@ export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): C crawlRules, sitemaps, entryPoints, + deduplicationEnabled, + deduplicationFields, + availableDeduplicationFields, }; if (lastCrawl) { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx index d6299bc1b3896..6e213edf457b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import type { EuiSearchBarOnChangeArgs } from '@elastic/eui'; @@ -71,7 +71,11 @@ export const RoleMappingsTable: React.FC = ({ return _rm; }) as SharedRoleMapping[]; - const [items, setItems] = useState(standardizedRoleMappings); + const [items, setItems] = useState([] as SharedRoleMapping[]); + + useEffect(() => { + setItems(standardizedRoleMappings); + }, [roleMappings]); const attributeNameCol: EuiBasicTableColumn = { field: 'attribute', diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/enzyme_rerender.ts b/x-pack/plugins/enterprise_search/public/applications/test_helpers/enzyme_rerender.ts index 70703b7017667..68b8791a0d087 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/enzyme_rerender.ts +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/enzyme_rerender.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { ShallowWrapper } from 'enzyme'; +import { CommonWrapper } from 'enzyme'; /** * Quick and easy helper for re-rendering a React component in Enzyme * after (e.g.) updating Kea values */ -export const rerender = (wrapper: ShallowWrapper) => { +export const rerender = (wrapper: CommonWrapper) => { wrapper.setProps({}); // Re-renders wrapper.update(); // Just in case }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 98d50c5fb5cea..f575ddb19ebdc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -11,7 +11,7 @@ import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; -import { setSuccessMessage } from '../../../../../shared/flash_messages'; +import { flashSuccessToast } from '../../../../../shared/flash_messages'; import { KibanaLogic } from '../../../../../shared/kibana'; import { AppLogic } from '../../../../app_logic'; import { @@ -90,7 +90,7 @@ export const AddSource: React.FC = (props) => { const goToFormSourceCreated = () => { KibanaLogic.values.navigateToUrl(`${getSourcesPath(SOURCES_PATH, isOrganization)}`); - setSuccessMessage(FORM_SOURCE_ADDED_SUCCESS_MESSAGE); + flashSuccessToast(FORM_SOURCE_ADDED_SUCCESS_MESSAGE); }; const header = ; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index a75e494aa2b1c..0aa7cbcf5f1c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -14,7 +14,7 @@ import { HttpFetchQuery } from 'src/core/public'; import { flashAPIErrors, - setSuccessMessage, + flashSuccessToast, clearFlashMessages, setErrorMessage, } from '../../../../../shared/flash_messages'; @@ -491,7 +491,7 @@ export const AddSourceLogic = kea { const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; const { mount } = new LogicMounter(DisplaySettingsLogic); const { searchResultConfig, exampleDocuments } = exampleResult; @@ -110,7 +110,7 @@ describe('DisplaySettingsLogic', () => { serverProps.searchResultConfig ); - expect(setSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('handles empty color', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index 556507d891dcb..28d10b1566b6c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -11,7 +11,7 @@ import { cloneDeep, isEqual, differenceBy } from 'lodash'; import { DropResult } from '@elastic/eui'; import { - setSuccessMessage, + flashSuccessToast, clearFlashMessages, flashAPIErrors, } from '../../../../../shared/flash_messages'; @@ -405,7 +405,7 @@ export const DisplaySettingsLogic = kea< } }, setServerResponseData: () => { - setSuccessMessage(SUCCESS_MESSAGE); + flashSuccessToast(SUCCESS_MESSAGE); }, toggleFieldEditorModal: () => { clearFlashMessages(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts index d642900aea169..142e50d52c9db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts @@ -43,7 +43,7 @@ describe('SchemaLogic', () => { const { clearFlashMessages, flashAPIErrors, - setSuccessMessage, + flashSuccessToast, setErrorMessage, } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SchemaLogic); @@ -371,7 +371,7 @@ describe('SchemaLogic', () => { } ); await nextTick(); - expect(setSuccessMessage).toHaveBeenCalledWith(SCHEMA_FIELD_ADDED_MESSAGE); + expect(flashSuccessToast).toHaveBeenCalledWith(SCHEMA_FIELD_ADDED_MESSAGE); expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); }); @@ -430,7 +430,7 @@ describe('SchemaLogic', () => { } ); await nextTick(); - expect(setSuccessMessage).toHaveBeenCalledWith(SCHEMA_UPDATED_MESSAGE); + expect(flashSuccessToast).toHaveBeenCalledWith(SCHEMA_UPDATED_MESSAGE); expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts index f43be974102b2..114d63a3ce142 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { ADD, UPDATE } from '../../../../../shared/constants/operations'; import { flashAPIErrors, - setSuccessMessage, + flashSuccessToast, setErrorMessage, clearFlashMessages, } from '../../../../../shared/flash_messages'; @@ -346,7 +346,7 @@ export const SchemaLogic = kea>({ body: JSON.stringify({ ...updatedSchema }), }); actions.onSchemaSetSuccess(response); - setSuccessMessage(successMessage); + flashSuccessToast(successMessage); } catch (e) { window.scrollTo(0, 0); if (isAdding) { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx index 7c4c02cdc9819..c779d76af5e75 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx @@ -29,7 +29,7 @@ describe('SourceInfoCard', () => { expect(wrapper.find(SourceIcon)).toHaveLength(1); expect(wrapper.find(EuiBadge)).toHaveLength(1); expect(wrapper.find(EuiHealth)).toHaveLength(1); - expect(wrapper.find(EuiText)).toHaveLength(3); + expect(wrapper.find(EuiText)).toHaveLength(1); expect(wrapper.find(EuiTitle)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx index d98b4f6b1e67d..e2c9cc05b04c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -64,16 +64,12 @@ export const SourceInfoCard: React.FC = ({
{isFederatedSource && ( - + - - {STATUS_LABEL} - + {STATUS_LABEL} - - {READY_TEXT} - + {READY_TEXT} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index 81d2803690161..adeddb08dcb79 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -29,8 +29,7 @@ describe('SourceLogic', () => { const { clearFlashMessages, flashAPIErrors, - setSuccessMessage, - setQueuedSuccessMessage, + flashSuccessToast, setErrorMessage, } = mockFlashMessageHelpers; const { navigateToUrl } = mockKibanaValues; @@ -79,7 +78,7 @@ describe('SourceLogic', () => { ...contentSource, name: NAME, }); - expect(setSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('setSearchResults', () => { @@ -391,7 +390,7 @@ describe('SourceLogic', () => { expect(clearFlashMessages).toHaveBeenCalled(); expect(http.delete).toHaveBeenCalledWith('/api/workplace_search/org/sources/123'); await promise; - expect(setQueuedSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); expect(setButtonNotLoadingSpy).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 4d145bf798160..6040f319357d9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -12,9 +12,8 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_META } from '../../../shared/constants'; import { flashAPIErrors, - setSuccessMessage, + flashSuccessToast, setErrorMessage, - setQueuedSuccessMessage, clearFlashMessages, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; @@ -239,7 +238,8 @@ export const SourceLogic = kea>({ try { const response = await HttpLogic.values.http.delete(route); - setQueuedSuccessMessage( + KibanaLogic.values.navigateToUrl(getSourcesPath(SOURCES_PATH, isOrganization)); + flashSuccessToast( i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.contentSourceRemoved', { @@ -248,7 +248,6 @@ export const SourceLogic = kea>({ } ) ); - KibanaLogic.values.navigateToUrl(getSourcesPath(SOURCES_PATH, isOrganization)); } catch (e) { flashAPIErrors(e); } finally { @@ -256,7 +255,7 @@ export const SourceLogic = kea>({ } }, onUpdateSourceName: (name: string) => { - setSuccessMessage( + flashSuccessToast( i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.contentSourceNameChanged', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts index 74d3faca5994b..bc18fade742aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts @@ -23,7 +23,7 @@ import { SourcesLogic, fetchSourceStatuses, POLLING_INTERVAL } from './sources_l describe('SourcesLogic', () => { const { http } = mockHttpValues; - const { flashAPIErrors, setQueuedSuccessMessage } = mockFlashMessageHelpers; + const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; const { mount, unmount } = new LogicMounter(SourcesLogic); const contentSource = contentSources[0]; @@ -126,7 +126,7 @@ describe('SourcesLogic', () => { additionalConfiguration: false, serviceType: 'custom', }); - expect(setQueuedSuccessMessage).toHaveBeenCalledWith('Successfully connected source. '); + expect(flashSuccessToast).toHaveBeenCalledWith('Successfully connected source. '); }); it('unconfigured', () => { @@ -138,7 +138,7 @@ describe('SourcesLogic', () => { additionalConfiguration: true, serviceType: 'custom', }); - expect(setQueuedSuccessMessage).toHaveBeenCalledWith( + expect(flashSuccessToast).toHaveBeenCalledWith( 'Successfully connected source. This source requires additional configuration.' ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts index 9de2b447619a6..14c79b75dff8e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -10,7 +10,7 @@ import { cloneDeep, findIndex } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; +import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { AppLogic } from '../../app_logic'; import { Connector, ContentSourceDetails, ContentSourceStatus, SourceDataItem } from '../../types'; @@ -222,7 +222,7 @@ export const SourcesLogic = kea>( } ); - setQueuedSuccessMessage( + flashSuccessToast( [ successfullyConnectedMessage, additionalConfiguration ? additionalConfigurationMessage : '', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts index 2e5a0b3d9b939..6184dada8f111 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts @@ -27,8 +27,7 @@ describe('GroupLogic', () => { const { clearFlashMessages, flashAPIErrors, - setSuccessMessage, - setQueuedSuccessMessage, + flashSuccessToast, setQueuedErrorMessage, } = mockFlashMessageHelpers; @@ -224,9 +223,7 @@ describe('GroupLogic', () => { await nextTick(); expect(navigateToUrl).toHaveBeenCalledWith(GROUPS_PATH); - expect(setQueuedSuccessMessage).toHaveBeenCalledWith( - 'Group "group" was successfully deleted.' - ); + expect(flashSuccessToast).toHaveBeenCalledWith('Group "group" was successfully deleted.'); }); it('handles error', async () => { @@ -255,7 +252,7 @@ describe('GroupLogic', () => { await nextTick(); expect(onGroupNameChangedSpy).toHaveBeenCalledWith(group); - expect(setSuccessMessage).toHaveBeenCalledWith( + expect(flashSuccessToast).toHaveBeenCalledWith( 'Successfully renamed this group to "group".' ); }); @@ -286,7 +283,7 @@ describe('GroupLogic', () => { await nextTick(); expect(onGroupSourcesSavedSpy).toHaveBeenCalledWith(group); - expect(setSuccessMessage).toHaveBeenCalledWith( + expect(flashSuccessToast).toHaveBeenCalledWith( 'Successfully updated shared content sources.' ); }); @@ -323,7 +320,7 @@ describe('GroupLogic', () => { }); await nextTick(); - expect(setSuccessMessage).toHaveBeenCalledWith( + expect(flashSuccessToast).toHaveBeenCalledWith( 'Successfully updated shared source prioritization.' ); expect(onGroupPrioritiesChangedSpy).toHaveBeenCalledWith(group); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts index 7f3e1d9f0b82d..f8ec50b309725 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts @@ -13,8 +13,7 @@ import { i18n } from '@kbn/i18n'; import { clearFlashMessages, flashAPIErrors, - setSuccessMessage, - setQueuedSuccessMessage, + flashSuccessToast, setQueuedErrorMessage, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; @@ -206,7 +205,7 @@ export const GroupLogic = kea>({ } ); - setQueuedSuccessMessage(GROUP_DELETED_MESSAGE); + flashSuccessToast(GROUP_DELETED_MESSAGE); KibanaLogic.values.navigateToUrl(GROUPS_PATH); } catch (e) { flashAPIErrors(e); @@ -231,7 +230,7 @@ export const GroupLogic = kea>({ values: { groupName: response.name }, } ); - setSuccessMessage(GROUP_RENAMED_MESSAGE); + flashSuccessToast(GROUP_RENAMED_MESSAGE); } catch (e) { flashAPIErrors(e); } @@ -256,7 +255,7 @@ export const GroupLogic = kea>({ defaultMessage: 'Successfully updated shared content sources.', } ); - setSuccessMessage(GROUP_SOURCES_UPDATED_MESSAGE); + flashSuccessToast(GROUP_SOURCES_UPDATED_MESSAGE); } catch (e) { flashAPIErrors(e); } @@ -289,7 +288,7 @@ export const GroupLogic = kea>({ } ); - setSuccessMessage(GROUP_PRIORITIZATION_UPDATED_MESSAGE); + flashSuccessToast(GROUP_PRIORITIZATION_UPDATED_MESSAGE); actions.onGroupPrioritiesChanged(response); } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts index a036cdda3d68e..36061bc18196b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts @@ -15,7 +15,7 @@ import { DEFAULT_META } from '../../../shared/constants'; import { clearFlashMessages, flashAPIErrors, - setSuccessMessage, + flashSuccessToast, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { ContentSource, Group, User } from '../../types'; @@ -328,7 +328,7 @@ export const GroupsLogic = kea>({ } ); - setSuccessMessage(SUCCESS_MESSAGE); + flashSuccessToast(SUCCESS_MESSAGE); actions.setNewGroup(response); } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts index 6e7104964cdb7..29b448bc0684a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -10,7 +10,7 @@ import { kea, MakeLogicType } from 'kea'; import { clearFlashMessages, flashAPIErrors, - setSuccessMessage, + flashSuccessToast, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { @@ -397,7 +397,7 @@ export const RoleMappingsLogic = kea> try { const response = await http.patch(route, { body }); actions.setSourceRestrictionsUpdated(response); - setSuccessMessage(SOURCE_RESTRICTIONS_SUCCESS_MESSAGE); + flashSuccessToast(SOURCE_RESTRICTIONS_SUCCESS_MESSAGE); AppLogic.actions.setSourceRestriction(isEnabled); } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx index b153aed607f77..30b450df91c17 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx @@ -123,6 +123,7 @@ export const BrandingSection: React.FC = ({ {`${BRAND_TEXT} { const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { - clearFlashMessages, - flashAPIErrors, - flashSuccessToast, - setQueuedSuccessMessage, - } = mockFlashMessageHelpers; + const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SettingsLogic); const ORG_NAME = 'myOrg'; const defaultValues = { @@ -309,7 +304,7 @@ describe('SettingsLogic', () => { await nextTick(); expect(navigateToUrl).toHaveBeenCalledWith('/settings/connectors'); - expect(setQueuedSuccessMessage).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); }); it('handles error', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts index c32ac9c84fd0a..886f81129ee17 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts @@ -11,7 +11,6 @@ import { i18n } from '@kbn/i18n'; import { clearFlashMessages, - setQueuedSuccessMessage, flashSuccessToast, flashAPIErrors, } from '../../../shared/flash_messages'; @@ -290,7 +289,7 @@ export const SettingsLogic = kea> try { await http.delete(route); KibanaLogic.values.navigateToUrl(ORG_SETTINGS_CONNECTORS_PATH); - setQueuedSuccessMessage( + flashSuccessToast( i18n.translate('xpack.enterpriseSearch.workplaceSearch.settings.configRemoved.message', { defaultMessage: 'Successfully removed configuration for {name}.', values: { name }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index 38cae6d5d7f7c..d50d7b7cee225 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -266,7 +266,7 @@ describe('crawler routes', () => { }); }); - it('validates correctly with required params', () => { + it('validates correctly with crawl rules', () => { const request = { params: { name: 'some-engine', id: '1234' }, body: { @@ -281,9 +281,24 @@ describe('crawler routes', () => { mockRouter.shouldValidate(request); }); - it('fails otherwise', () => { - const request = { params: {}, body: {} }; - mockRouter.shouldThrow(request); + it('validates correctly with deduplication enabled', () => { + const request = { + params: { name: 'some-engine', id: '1234' }, + body: { + deduplication_enabled: true, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('validates correctly with deduplication fields', () => { + const request = { + params: { name: 'some-engine', id: '1234' }, + body: { + deduplication_fields: ['title', 'description'], + }, + }; + mockRouter.shouldValidate(request); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index 79664d45dbbd8..cf90ffdea412a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -136,12 +136,16 @@ export function registerCrawlerRoutes({ id: schema.string(), }), body: schema.object({ - crawl_rules: schema.arrayOf( - schema.object({ - order: schema.number(), - id: schema.string(), - }) + crawl_rules: schema.maybe( + schema.arrayOf( + schema.object({ + order: schema.number(), + id: schema.string(), + }) + ) ), + deduplication_enabled: schema.maybe(schema.boolean()), + deduplication_fields: schema.maybe(schema.arrayOf(schema.string())), }), }, }, diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 682bf2660c78b..33c7fcabc9f46 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -145,6 +145,7 @@ Below is a document in the expected structure, with descriptions of the fields: type: " saved object type", }, ], + version: "7.15.0" }, } ``` diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 0f5f4af2052ee..cbb59cc3204c0 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -291,6 +291,9 @@ "ignore_above": 1024 } } + }, + "version": { + "type": "version" } } } diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 556ddec5a7001..7cecba1efa438 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -13,6 +13,7 @@ // the event log import { schema, TypeOf } from '@kbn/config-schema'; +import semver from 'semver'; type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable }; type DeepPartial = { @@ -126,6 +127,7 @@ export const EventSchema = schema.maybe( }) ) ), + version: ecsVersion(), }) ), }) @@ -147,9 +149,18 @@ function ecsDate() { return schema.maybe(schema.string({ validate: validateDate })); } +function ecsVersion() { + return schema.maybe(schema.string({ validate: validateVersion })); +} + const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; function validateDate(isoDate: string) { if (ISO_DATE_PATTERN.test(isoDate)) return; return 'string is not a valid ISO date: ' + isoDate; } + +function validateVersion(version: string) { + if (semver.valid(version)) return; + return 'string is not a valid version: ' + version; +} diff --git a/x-pack/plugins/event_log/kibana.json b/x-pack/plugins/event_log/kibana.json index 0231bb6234471..5223549a2e4fb 100644 --- a/x-pack/plugins/event_log/kibana.json +++ b/x-pack/plugins/event_log/kibana.json @@ -2,6 +2,10 @@ "id": "eventLog", "version": "0.0.1", "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "configPath": ["xpack", "eventLog"], "optionalPlugins": ["spaces"], "server": true, diff --git a/x-pack/plugins/event_log/scripts/create_schemas.js b/x-pack/plugins/event_log/scripts/create_schemas.js index 4b91cf6a73622..0a53165630a29 100755 --- a/x-pack/plugins/event_log/scripts/create_schemas.js +++ b/x-pack/plugins/event_log/scripts/create_schemas.js @@ -150,6 +150,11 @@ function generateSchemaLines(lineWriter, prop, mappings) { return; } + if (mappings.type === 'version') { + lineWriter.addLine(`${propKey}: ecsVersion(),`); + return; + } + // only handling objects for the rest of this function if (mappings.properties == null) { logError(`unknown properties to map: ${prop}: ${JSON.stringify(mappings)}`); @@ -314,6 +319,15 @@ function validateDate(isoDate: string) { if (ISO_DATE_PATTERN.test(isoDate)) return; return 'string is not a valid ISO date: ' + isoDate; } + +function ecsVersion() { + return schema.maybe(schema.string({ validate: validateVersion })); +} + +function validateVersion(version: string) { + if (semver.valid(version)) return; + return 'string is not a valid version: ' + version; +} `.trim(); function getSchemaFileContents(ecsVersion, schemaLines) { diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 93fe053bd0cdf..d114603052491 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -77,6 +77,9 @@ exports.EcsCustomPropertyMappings = { }, }, }, + version: { + type: 'version', + }, }, }, }; diff --git a/x-pack/plugins/event_log/server/event_log_service.test.ts b/x-pack/plugins/event_log/server/event_log_service.test.ts index dc7ac70e2f0ce..ad8b8c06b47f3 100644 --- a/x-pack/plugins/event_log/server/event_log_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_service.test.ts @@ -30,6 +30,7 @@ describe('EventLogService', () => { logEntries, indexEntries, }, + kibanaVersion: '1.0.1', }); } @@ -75,6 +76,7 @@ describe('EventLogService', () => { logEntries: true, indexEntries: true, }, + kibanaVersion: '1.0.1', }; const service = new EventLogService(params); @@ -113,6 +115,7 @@ describe('EventLogService', () => { logEntries: true, indexEntries: true, }, + kibanaVersion: '1.0.1', }; const service = new EventLogService(params); const eventLogger = service.getLogger({}); @@ -131,6 +134,7 @@ describe('EventLogService', () => { logEntries: true, indexEntries: true, }, + kibanaVersion: '1.0.1', }; const service = new EventLogService(params); const provider = jest.fn(); diff --git a/x-pack/plugins/event_log/server/event_log_service.ts b/x-pack/plugins/event_log/server/event_log_service.ts index 96462938e0bf8..f6e1533aa1155 100644 --- a/x-pack/plugins/event_log/server/event_log_service.ts +++ b/x-pack/plugins/event_log/server/event_log_service.ts @@ -6,7 +6,7 @@ */ import { Observable } from 'rxjs'; -import { IClusterClient } from 'src/core/server'; +import { IClusterClient, PluginInitializerContext } from 'src/core/server'; import { Plugin } from './plugin'; import { EsContext } from './es'; @@ -24,6 +24,7 @@ interface EventLogServiceCtorParams { kibanaUUID: string; systemLogger: SystemLogger; savedObjectProviderRegistry: SavedObjectProviderRegistry; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; } // note that clusterClient may be null, indicating we can't write to ES @@ -34,6 +35,7 @@ export class EventLogService implements IEventLogService { private registeredProviderActions: Map>; private savedObjectProviderRegistry: SavedObjectProviderRegistry; + public readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; public readonly kibanaUUID: string; constructor({ @@ -42,6 +44,7 @@ export class EventLogService implements IEventLogService { kibanaUUID, systemLogger, savedObjectProviderRegistry, + kibanaVersion, }: EventLogServiceCtorParams) { this.config = config; this.esContext = esContext; @@ -49,6 +52,7 @@ export class EventLogService implements IEventLogService { this.systemLogger = systemLogger; this.registeredProviderActions = new Map>(); this.savedObjectProviderRegistry = savedObjectProviderRegistry; + this.kibanaVersion = kibanaVersion; } public isEnabled(): boolean { diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index 4790ca2eba94b..9dcfa37ae76b1 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -34,6 +34,7 @@ describe('EventLogger', () => { config: { enabled: true, logEntries: true, indexEntries: true }, kibanaUUID: KIBANA_SERVER_UUID, savedObjectProviderRegistry: savedObjectProviderRegistryMock.create(), + kibanaVersion: '1.0.1', }); eventLogger = service.getLogger({}); }); @@ -86,6 +87,7 @@ describe('EventLogger', () => { }, kibana: { server_uuid: '424-24-2424', + version: '1.0.1', }, }); diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index b985a173ccdbf..bcda73da215ae 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; import { merge } from 'lodash'; +import { coerce } from 'semver'; import { Plugin } from './plugin'; import { EsContext } from './es'; import { EventLogService } from './event_log_service'; @@ -74,6 +75,7 @@ export class EventLogger implements IEventLogger { }, kibana: { server_uuid: this.eventLogService.kibanaUUID, + version: coerce(this.eventLogService.kibanaVersion)?.version, }, }; diff --git a/x-pack/plugins/event_log/server/plugin.test.ts b/x-pack/plugins/event_log/server/plugin.test.ts index cbe45aeb64334..166b084deb6bf 100644 --- a/x-pack/plugins/event_log/server/plugin.test.ts +++ b/x-pack/plugins/event_log/server/plugin.test.ts @@ -12,13 +12,13 @@ import { Plugin } from './plugin'; import { spacesMock } from '../../spaces/server/mocks'; describe('event_log plugin', () => { - it('can setup and start', async () => { + it('can setup and start', () => { const initializerContext = coreMock.createPluginInitializerContext({}); const coreSetup = coreMock.createSetup() as CoreSetup; const coreStart = coreMock.createStart() as CoreStart; const plugin = new Plugin(initializerContext); - const setup = await plugin.setup(coreSetup); + const setup = plugin.setup(coreSetup); expect(typeof setup.getLogger).toBe('function'); expect(typeof setup.getProviderActions).toBe('function'); expect(typeof setup.isEnabled).toBe('function'); @@ -29,7 +29,7 @@ describe('event_log plugin', () => { expect(typeof setup.registerSavedObjectProvider).toBe('function'); const spaces = spacesMock.createStart(); - const start = await plugin.start(coreStart, { spaces }); + const start = plugin.start(coreStart, { spaces }); expect(typeof start.getClient).toBe('function'); }); @@ -41,8 +41,8 @@ describe('event_log plugin', () => { const plugin = new Plugin(initializerContext); const spaces = spacesMock.createStart(); - await plugin.setup(coreSetup); - await plugin.start(coreStart, { spaces }); + plugin.setup(coreSetup); + plugin.start(coreStart, { spaces }); await plugin.stop(); expect(mockLogger.debug).toBeCalledWith('shutdown: waiting to finish'); expect(mockLogger.debug).toBeCalledWith('shutdown: finished'); diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 9cc874735cc0e..77cad86cefdc6 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -84,6 +84,7 @@ export class Plugin implements CorePlugin { + logger.warn(`Failure to install package [${pkgName}]: [${err.toString()}]`); await handleInstallPackageFailure({ savedObjectsClient, error: err, diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 573e1847f8eb3..561cbef952f8d 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -7,6 +7,7 @@ import { omit } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { getFlattenedObject } from '@kbn/std'; import type { KibanaRequest } from 'src/core/server'; import type { ElasticsearchClient, @@ -21,6 +22,8 @@ import { packageToPackagePolicyInputs, isPackageLimited, doesAgentPolicyAlreadyIncludePackage, + validatePackagePolicy, + validationHasErrors, } from '../../common'; import type { DeletePackagePoliciesResponse, @@ -442,7 +445,11 @@ class PackagePolicyService { return result; } - public async getUpgradePackagePolicyInfo(soClient: SavedObjectsClientContract, id: string) { + public async getUpgradePackagePolicyInfo( + soClient: SavedObjectsClientContract, + id: string, + packageVersion?: string + ) { const packagePolicy = await this.get(soClient, id); if (!packagePolicy) { throw new Error( @@ -462,28 +469,30 @@ class PackagePolicyService { ); } - const installedPackage = await getInstallation({ - savedObjectsClient: soClient, - pkgName: packagePolicy.package.name, - }); - if (!installedPackage) { - throw new Error( - i18n.translate('xpack.fleet.packagePolicy.packageNotInstalledError', { - defaultMessage: 'Cannot upgrade package policy {id} because {pkgName} is not installed', - values: { id, pkgName: packagePolicy.package.name }, - }) - ); - } + let packageInfo: PackageInfo; - const installedPkgInfo = await getPackageInfo({ - savedObjectsClient: soClient, - pkgName: packagePolicy.package.name, - pkgVersion: installedPackage.version, - }); + if (packageVersion) { + packageInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: packageVersion, + }); + } else { + const installedPackage = await getInstallation({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + }); + + packageInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: installedPackage?.version ?? '', + }); + } return { packagePolicy: packagePolicy as Required, - installedPkgInfo, + packageInfo, }; } @@ -497,25 +506,23 @@ class PackagePolicyService { for (const id of ids) { try { - const { packagePolicy, installedPkgInfo } = await this.getUpgradePackagePolicyInfo( - soClient, - id - ); + const { packagePolicy, packageInfo } = await this.getUpgradePackagePolicyInfo(soClient, id); const updatePackagePolicy = overridePackageInputs( { ...omit(packagePolicy, 'id'), - inputs: packageToPackagePolicyInputs(installedPkgInfo), + inputs: packagePolicy.inputs, package: { ...packagePolicy.package, - version: installedPkgInfo.version, + version: packageInfo.version, }, }, - packagePolicy.inputs as InputsOverride[] + packageInfo, + packageToPackagePolicyInputs(packageInfo) as InputsOverride[] ); updatePackagePolicy.inputs = await this.compilePackagePolicyInputs( - installedPkgInfo, + packageInfo, updatePackagePolicy.vars || {}, updatePackagePolicy.inputs as PackagePolicyInput[] ); @@ -546,29 +553,32 @@ class PackagePolicyService { public async getUpgradeDryRunDiff( soClient: SavedObjectsClientContract, - id: string + id: string, + packageVersion?: string ): Promise { try { - const { packagePolicy, installedPkgInfo } = await this.getUpgradePackagePolicyInfo( + const { packagePolicy, packageInfo } = await this.getUpgradePackagePolicyInfo( soClient, - id + id, + packageVersion ); const updatedPackagePolicy = overridePackageInputs( { ...omit(packagePolicy, 'id'), - inputs: packageToPackagePolicyInputs(installedPkgInfo), + inputs: packagePolicy.inputs, package: { ...packagePolicy.package, - version: installedPkgInfo.version, + version: packageInfo.version, }, }, - packagePolicy.inputs as InputsOverride[], + packageInfo, + packageToPackagePolicyInputs(packageInfo) as InputsOverride[], true ); updatedPackagePolicy.inputs = await this.compilePackagePolicyInputs( - installedPkgInfo, + packageInfo, updatedPackagePolicy.vars || {}, updatedPackagePolicy.inputs as PackagePolicyInput[] ); @@ -849,6 +859,7 @@ export type { PackagePolicyService }; export function overridePackageInputs( basePackagePolicy: NewPackagePolicy, + packageInfo: PackageInfo, inputsOverride?: InputsOverride[], dryRun?: boolean ): DryRunPackagePolicy { @@ -856,11 +867,11 @@ export function overridePackageInputs( const inputs = [...basePackagePolicy.inputs]; const packageName = basePackagePolicy.package!.name; - const errors = []; - let responseMissingVars: string[] = []; + let errors = []; for (const override of inputsOverride) { let originalInput = inputs.find((i) => i.type === override.type); + if (!originalInput) { const e = { error: new Error( @@ -874,13 +885,16 @@ export function overridePackageInputs( ), package: { name: packageName, version: basePackagePolicy.package!.version }, }; + if (dryRun) { errors.push({ key: override.type, message: String(e.error), }); continue; - } else throw e; + } else { + throw e; + } } if (typeof override.enabled !== 'undefined') originalInput.enabled = override.enabled; @@ -888,33 +902,7 @@ export function overridePackageInputs( originalInput.keep_enabled = override.keep_enabled; if (override.vars) { - try { - const { result, missingVars } = deepMergeVars(override, originalInput); - originalInput = result; - responseMissingVars = [...responseMissingVars, ...missingVars]; - } catch (e) { - const varName = e.message; - const err = { - error: new Error( - i18n.translate('xpack.fleet.packagePolicyVarOverrideError', { - defaultMessage: - 'Var {varName} does not exist on {inputType} of package {packageName}', - values: { - varName, - inputType: override.type, - packageName, - }, - }) - ), - package: { name: packageName, version: basePackagePolicy.package!.version }, - }; - if (dryRun) { - errors.push({ - key: `${override.type}.vars.${varName}`, - message: String(err.error), - }); - } else throw err; - } + originalInput = deepMergeVars(originalInput, override); } if (override.streams) { @@ -922,6 +910,7 @@ export function overridePackageInputs( let originalStream = originalInput?.streams.find( (s) => s.data_stream.dataset === stream.data_stream.dataset ); + if (!originalStream) { const streamSet = stream.data_stream.dataset; const e = { @@ -938,62 +927,61 @@ export function overridePackageInputs( ), package: { name: packageName, version: basePackagePolicy.package!.version }, }; + if (dryRun) { errors.push({ key: `${override.type}.streams.${streamSet}`, message: String(e.error), }); + continue; - } else throw e; + } else { + throw e; + } } - if (typeof stream.enabled !== 'undefined') originalStream.enabled = stream.enabled; + if (typeof stream.enabled !== 'undefined') { + originalStream.enabled = stream.enabled; + } if (stream.vars) { - try { - const { result, missingVars } = deepMergeVars(stream as InputsOverride, originalStream); - originalStream = result; - responseMissingVars = [...responseMissingVars, ...missingVars]; - } catch (e) { - const varName = e.message; - const streamSet = stream.data_stream.dataset; - const err = { - error: new Error( - i18n.translate('xpack.fleet.packagePolicyStreamVarOverrideError', { - defaultMessage: - 'Var {varName} does not exist on {streamSet} for {inputType} of package {packageName}', - values: { - varName, - streamSet, - inputType: override.type, - packageName, - }, - }) - ), - package: { name: packageName, version: basePackagePolicy.package!.version }, - }; - if (dryRun) { - errors.push({ - key: `${override.type}.streams.${streamSet}.${varName}`, - message: String(err.error), - }); - } else throw err; - } + originalStream = deepMergeVars(originalStream, stream as InputsOverride); } } } } + const resultingPackagePolicy: NewPackagePolicy = { + ...basePackagePolicy, + inputs, + }; + + const validationResults = validatePackagePolicy(resultingPackagePolicy, packageInfo); + + if (validationHasErrors(validationResults)) { + const responseFormattedValidationErrors = Object.entries(getFlattenedObject(validationResults)) + .map(([key, value]) => ({ + key, + message: value, + })) + .filter(({ message }) => !!message); + + errors = [...errors, ...responseFormattedValidationErrors]; + } + if (dryRun && errors.length) { - return { ...basePackagePolicy, inputs, errors, missingVars: responseMissingVars }; + return { ...resultingPackagePolicy, errors }; } - return { ...basePackagePolicy, inputs, missingVars: responseMissingVars }; + return resultingPackagePolicy; } -function deepMergeVars(override: any, original: any): { result: any; missingVars: string[] } { +function deepMergeVars(original: any, override: any): any { const result = { ...original }; - const missingVars: string[] = []; + + if (!result.vars || !override.vars) { + return; + } const overrideVars = Array.isArray(override.vars) ? override.vars @@ -1002,15 +990,15 @@ function deepMergeVars(override: any, original: any): { result: any; missingVars ...(rest as any), })); - for (const { name, ...val } of overrideVars) { - if (!original.vars || !(name in original.vars)) { - missingVars.push(name); - continue; + for (const { name, ...overrideVal } of overrideVars) { + const originalVar = original.vars[name]; + + if (!result.vars) { + result.vars = {}; } - const originalVar = original.vars[name]; - result[name] = { ...originalVar, ...val }; + result.vars[name] = { ...overrideVal, ...originalVar }; } - return { result, missingVars }; + return result; } diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 334df17a8d3a8..37ed98a6f4aa0 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -28,7 +28,7 @@ import { import { escapeSearchQueryPhrase } from './saved_object'; import { pkgToPkgKey } from './epm/registry'; -import { getInstallation } from './epm/packages'; +import { getInstallation, getPackageInfo } from './epm/packages'; import { ensurePackagesCompletedInstall } from './epm/packages/install'; import { bulkInstallPackages } from './epm/packages/bulk_install_packages'; import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; @@ -277,6 +277,12 @@ async function addPreconfiguredPolicyPackages( ) { // Add packages synchronously to avoid overwriting for (const { installedPackage, name, description, inputs } of installedPackagePolicies) { + const packageInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: installedPackage.name, + pkgVersion: installedPackage.version, + }); + await addPackageToAgentPolicy( soClient, esClient, @@ -285,7 +291,7 @@ async function addPreconfiguredPolicyPackages( defaultOutput, name, description, - (policy) => overridePackageInputs(policy, inputs) + (policy) => overridePackageInputs(policy, packageInfo, inputs) ); } } diff --git a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts index a88316e8e7574..4ccc57aca0ebd 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts @@ -41,5 +41,6 @@ export const UpgradePackagePoliciesRequestSchema = { body: schema.object({ packagePolicyIds: schema.arrayOf(schema.string()), dryRun: schema.maybe(schema.boolean()), + packageVersion: schema.maybe(schema.string()), }), }; diff --git a/x-pack/plugins/global_search/server/services/context.mock.ts b/x-pack/plugins/global_search/server/services/context.mock.ts index c5cf3ef49b9df..c7f43ad5b8ff0 100644 --- a/x-pack/plugins/global_search/server/services/context.mock.ts +++ b/x-pack/plugins/global_search/server/services/context.mock.ts @@ -10,7 +10,6 @@ import { Capabilities } from 'src/core/server'; import { savedObjectsTypeRegistryMock, savedObjectsClientMock, - elasticsearchServiceMock, uiSettingsServiceMock, capabilitiesServiceMock, } from '../../../../../src/core/server/mocks'; @@ -22,11 +21,6 @@ const createContextMock = (capabilities: Partial = {}) => { client: savedObjectsClientMock.create(), typeRegistry: savedObjectsTypeRegistryMock.create(), }, - elasticsearch: { - legacy: { - client: elasticsearchServiceMock.createLegacyScopedClusterClient(), - }, - }, uiSettings: { client: uiSettingsServiceMock.createClient(), }, diff --git a/x-pack/plugins/global_search/server/services/context.test.ts b/x-pack/plugins/global_search/server/services/context.test.ts index 4fefc009fe1ec..580083efbb303 100644 --- a/x-pack/plugins/global_search/server/services/context.test.ts +++ b/x-pack/plugins/global_search/server/services/context.test.ts @@ -21,9 +21,6 @@ describe('getContextFactory', () => { expect(coreStart.savedObjects.getTypeRegistry).toHaveBeenCalledTimes(1); - expect(coreStart.elasticsearch.legacy.client.asScoped).toHaveBeenCalledTimes(1); - expect(coreStart.elasticsearch.legacy.client.asScoped).toHaveBeenCalledWith(request); - const soClient = coreStart.savedObjects.getScopedClient.mock.results[0].value; expect(coreStart.uiSettings.asScopedToClient).toHaveBeenCalledTimes(1); expect(coreStart.uiSettings.asScopedToClient).toHaveBeenCalledWith(soClient); @@ -34,7 +31,6 @@ describe('getContextFactory', () => { expect(context).toEqual({ core: { savedObjects: expect.any(Object), - elasticsearch: expect.any(Object), uiSettings: expect.any(Object), capabilities: expect.any(Object), }, diff --git a/x-pack/plugins/global_search/server/services/context.ts b/x-pack/plugins/global_search/server/services/context.ts index f42a8417b3c25..454b0324ab3c1 100644 --- a/x-pack/plugins/global_search/server/services/context.ts +++ b/x-pack/plugins/global_search/server/services/context.ts @@ -24,11 +24,6 @@ export const getContextFactory = (coreStart: CoreStart) => ( client: soClient, typeRegistry: coreStart.savedObjects.getTypeRegistry(), }, - elasticsearch: { - legacy: { - client: coreStart.elasticsearch.legacy.client.asScoped(request), - }, - }, uiSettings: { client: coreStart.uiSettings.asScopedToClient(soClient), }, diff --git a/x-pack/plugins/global_search/server/types.ts b/x-pack/plugins/global_search/server/types.ts index 1732df69f997f..363f837f4bd2e 100644 --- a/x-pack/plugins/global_search/server/types.ts +++ b/x-pack/plugins/global_search/server/types.ts @@ -8,7 +8,6 @@ import { Observable } from 'rxjs'; import type { ISavedObjectTypeRegistry, - ILegacyScopedClusterClient, IUiSettingsClient, SavedObjectsClientContract, Capabilities, @@ -68,11 +67,6 @@ export interface GlobalSearchProviderContext { client: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; }; - elasticsearch: { - legacy: { - client: ILegacyScopedClusterClient; - }; - }; uiSettings: { client: IUiSettingsClient; }; diff --git a/x-pack/plugins/grokdebugger/kibana.json b/x-pack/plugins/grokdebugger/kibana.json index 5f288e0cf3bdb..692aa16329d54 100644 --- a/x-pack/plugins/grokdebugger/kibana.json +++ b/x-pack/plugins/grokdebugger/kibana.json @@ -2,16 +2,13 @@ "id": "grokdebugger", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": [ - "licensing", - "home", - "devTools" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "home", "devTools"], "server": true, "ui": true, "configPath": ["xpack", "grokdebugger"], - "requiredBundles": [ - "kibanaReact", - "esUiShared" - ] + "requiredBundles": ["kibanaReact", "esUiShared"] } diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json index 21e7e7888acb9..bccb3cd78e78d 100644 --- a/x-pack/plugins/index_lifecycle_management/kibana.json +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -3,23 +3,12 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "licensing", - "management", - "features", - "share" - ], - "optionalPlugins": [ - "cloud", - "usageCollection", - "indexManagement", - "home" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["licensing", "management", "features", "share"], + "optionalPlugins": ["cloud", "usageCollection", "indexManagement", "home"], "configPath": ["xpack", "ilm"], - "requiredBundles": [ - "indexManagement", - "kibanaReact", - "esUiShared", - "home" - ] + "requiredBundles": ["indexManagement", "kibanaReact", "esUiShared", "home"] } diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index cd29e7b9ee1cd..456ce830f6b57 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -1,14 +1,14 @@ { "id": "indexManagement", + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "version": "kibana", "server": true, "ui": true, "requiredPlugins": ["home", "management", "features", "share"], "optionalPlugins": ["security", "usageCollection", "fleet"], "configPath": ["xpack", "index_management"], - "requiredBundles": [ - "kibanaReact", - "esUiShared", - "runtimeFields" - ] + "requiredBundles": ["kibanaReact", "esUiShared", "runtimeFields"] } diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 7c54d18fbd382..800d92b5c9748 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -3,6 +3,10 @@ "version": "8.0.0", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["management", "features", "share"], "optionalPlugins": ["security", "usageCollection"], "configPath": ["xpack", "ingest_pipelines"], diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx new file mode 100644 index 0000000000000..75468f31b1a54 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +const DOT_EXPANDER_TYPE = 'dot_expander'; + +describe('Processor: Dot Expander', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(DOT_EXPANDER_TYPE); + }); + + test('prevents form submission if required fields are not provided', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with only the type defined + await saveNewProcessor(); + + // Expect form error as "field" is a required parameter + expect(form.getErrorsMessages()).toEqual(['A field value is required.']); + }); + + test('prevents form submission if field does not contain a . for the dot notation', async () => { + const { + actions: { saveNewProcessor }, + form, + component, + } = testBed; + + // Add invalid "field" value (required) + form.setInputValue('fieldNameField.input', 'missingTheDot'); + + // Save the processor with invalid field + await saveNewProcessor(); + + // Move ahead the debounce time which will then execute any validations + await act(async () => { + jest.runAllTimers(); + }); + component.update(); + + // Expect form error as "field" does not contain '.' + expect(form.getErrorsMessages()).toEqual([ + 'A field value requires at least one dot character.', + ]); + }); + test('saves with default parameter values', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field.with.dot'); + + // Save the field + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, DOT_EXPANDER_TYPE); + expect(processors[0][DOT_EXPANDER_TYPE]).toEqual({ + field: 'field.with.dot', + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field.notation'); + + // Set optional parameters + form.setInputValue('pathField.input', 'somepath'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, DOT_EXPANDER_TYPE); + expect(processors[0][DOT_EXPANDER_TYPE]).toEqual({ + field: 'field.notation', + path: 'somepath', + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index 9101e64278dc6..65d9b8f306058 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -146,6 +146,7 @@ type TestSubject = | 'fieldNameField.input' | 'messageField.input' | 'mockCodeEditor' + | 'pathField.input' | 'tagField.input' | 'typeSelectorField' | 'dateRoundingField' diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx index 4bbc242cf0ef8..c66633dfd23d5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx @@ -54,7 +54,12 @@ export const DotExpander: FunctionComponent = () => { ]} /> - + ); }; diff --git a/x-pack/plugins/lens/common/embeddable_factory/index.ts b/x-pack/plugins/lens/common/embeddable_factory/index.ts new file mode 100644 index 0000000000000..1eaa1dddfdf08 --- /dev/null +++ b/x-pack/plugins/lens/common/embeddable_factory/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SerializableRecord, Serializable } from '@kbn/utility-types'; +import { SavedObjectReference } from 'src/core/types'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; +import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; + +export type LensEmbeddablePersistableState = EmbeddableStateWithType & { + attributes: SerializableRecord; +}; + +export const inject: EmbeddableRegistryDefinition['inject'] = (state, references) => { + const typedState = state as LensEmbeddablePersistableState; + + if ('attributes' in typedState && typedState.attributes !== undefined) { + typedState.attributes.references = (references as unknown) as Serializable[]; + } + + return typedState; +}; + +export const extract: EmbeddableRegistryDefinition['extract'] = (state) => { + let references: SavedObjectReference[] = []; + const typedState = state as LensEmbeddablePersistableState; + + if ('attributes' in typedState && typedState.attributes !== undefined) { + references = (typedState.attributes.references as unknown) as SavedObjectReference[]; + } + + return { state, references }; +}; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 5a783bc4180d3..6bbc1284a0f1e 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -147,6 +147,7 @@ export async function mountApp( if (stateTransfer && props?.input) { const { input, isCopied } = props; stateTransfer.navigateToWithEmbeddablePackage(embeddableEditorIncomingState?.originatingApp, { + path: embeddableEditorIncomingState?.originatingPath, state: { embeddableId: isCopied ? undefined : embeddableEditorIncomingState.embeddableId, type: LENS_EMBEDDABLE_TYPE, @@ -155,7 +156,9 @@ export async function mountApp( }, }); } else { - coreStart.application.navigateToApp(embeddableEditorIncomingState?.originatingApp); + coreStart.application.navigateToApp(embeddableEditorIncomingState?.originatingApp, { + path: embeddableEditorIncomingState?.originatingPath, + }); } }; const initialContext = diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index 4cc074b5e830c..dcb72455e0ee9 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -5,11 +5,10 @@ * 2.0. */ -import type { Capabilities, HttpSetup, SavedObjectReference } from 'kibana/public'; +import type { Capabilities, HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Ast } from '@kbn/interpreter/target/common'; -import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { IndexPatternsContract, TimefilterContract } from '../../../../../src/plugins/data/public'; import { ReactExpressionRendererType } from '../../../../../src/plugins/expressions/public'; @@ -23,6 +22,7 @@ import { Document } from '../persistence/saved_object_store'; import { LensAttributeService } from '../lens_attribute_service'; import { DOC_TYPE } from '../../common'; import { ErrorMessage } from '../editor_frame_service/types'; +import { extract, inject } from '../../common/embeddable_factory'; export interface LensEmbeddableStartServices { timefilter: TimefilterContract; @@ -112,14 +112,6 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { ); } - extract(state: EmbeddableStateWithType) { - let references: SavedObjectReference[] = []; - const typedState = (state as unknown) as LensEmbeddableInput; - - if ('attributes' in typedState && typedState.attributes !== undefined) { - references = typedState.attributes.references; - } - - return { state, references }; - } + extract = extract; + inject = inject; } diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index e3da4bfe7fe72..d38afc17b2b07 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -285,7 +285,7 @@ export const HeatmapComponent: FC = ({ yAxisLabel: { visible: !!yAxisColumn && args.gridConfig.isYAxisLabelVisible, // eui color subdued - fill: chartTheme.axes?.tickLabel?.fill ?? '#6a717d', + textColor: chartTheme.axes?.tickLabel?.fill ?? '#6a717d', padding: yAxisColumn?.name ? 8 : 0, name: yAxisColumn?.name ?? '', ...(yAxisColumn @@ -297,7 +297,7 @@ export const HeatmapComponent: FC = ({ xAxisLabel: { visible: args.gridConfig.isXAxisLabelVisible, // eui color subdued - fill: chartTheme.axes?.tickLabel?.fill ?? `#6a717d`, + textColor: chartTheme.axes?.tickLabel?.fill ?? `#6a717d`, formatter: (v: number | string) => xValuesFormatter.convert(v), name: xAxisColumn.name, }, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index e0a4848974237..6e8b7d35b0cb9 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -130,7 +130,14 @@ export interface LensPublicStart { * * @experimental */ - navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => void; + navigateToPrefilledEditor: ( + input: LensEmbeddableInput | undefined, + options?: { + openInNewTab?: boolean; + originatingApp?: string; + originatingPath?: string; + } + ) => void; /** * Method which returns true if the user has permission to use Lens as defined by application capabilities. */ @@ -336,20 +343,24 @@ export class LensPlugin { return { EmbeddableComponent: getEmbeddableComponent(core, startDependencies), SaveModalComponent: getSaveModalComponent(core, startDependencies, this.attributeService!), - navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => { + navigateToPrefilledEditor: ( + input, + { openInNewTab = false, originatingApp = '', originatingPath } = {} + ) => { // for openInNewTab, we set the time range in url via getEditPath below - if (input.timeRange && !openInNewTab) { + if (input?.timeRange && !openInNewTab) { startDependencies.data.query.timefilter.timefilter.setTime(input.timeRange); } const transfer = new EmbeddableStateTransfer( core.application.navigateToApp, core.application.currentAppId$ ); - transfer.navigateToEditor('lens', { + transfer.navigateToEditor(APP_ID, { openInNewTab, - path: getEditPath(undefined, openInNewTab ? input.timeRange : undefined), + path: getEditPath(undefined, (openInNewTab && input?.timeRange) || undefined), state: { - originatingApp: '', + originatingApp, + originatingPath, valueInput: input, }, }); diff --git a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts index 14a9713d8461e..86a3a600b58ab 100644 --- a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts @@ -19,6 +19,7 @@ import { LensDocShapePre712, VisStatePre715, } from '../migrations/types'; +import { extract, inject } from '../../common/embeddable_factory'; export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { return { @@ -50,5 +51,7 @@ export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { } as unknown) as SerializableRecord; }, }, + extract, + inject, }; }; diff --git a/x-pack/plugins/lens/server/index.ts b/x-pack/plugins/lens/server/index.ts index b61282c9e26e5..f8a9b2452de41 100644 --- a/x-pack/plugins/lens/server/index.ts +++ b/x-pack/plugins/lens/server/index.ts @@ -8,7 +8,9 @@ import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; import { LensServerPlugin } from './plugin'; +export type { LensServerPluginSetup } from './plugin'; export * from './plugin'; +export * from './migrations/types'; import { configSchema, ConfigSchema } from '../config'; diff --git a/x-pack/plugins/lens/server/plugin.tsx b/x-pack/plugins/lens/server/plugin.tsx index f0ee801ece89b..e242fc8e4c5d6 100644 --- a/x-pack/plugins/lens/server/plugin.tsx +++ b/x-pack/plugins/lens/server/plugin.tsx @@ -36,7 +36,11 @@ export interface PluginStartContract { data: DataPluginStart; } -export class LensServerPlugin implements Plugin<{}, {}, {}, {}> { +export interface LensServerPluginSetup { + lensEmbeddableFactory: typeof lensEmbeddableFactory; +} + +export class LensServerPlugin implements Plugin { private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; private readonly telemetryLogger: Logger; @@ -63,8 +67,11 @@ export class LensServerPlugin implements Plugin<{}, {}, {}, {}> { plugins.taskManager ); } + plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory()); - return {}; + return { + lensEmbeddableFactory, + }; } start(core: CoreStart, plugins: PluginStartContract) { diff --git a/x-pack/plugins/license_api_guard/kibana.json b/x-pack/plugins/license_api_guard/kibana.json index 0fdf7ffed8988..1b870810ccbed 100644 --- a/x-pack/plugins/license_api_guard/kibana.json +++ b/x-pack/plugins/license_api_guard/kibana.json @@ -2,6 +2,10 @@ "id": "licenseApiGuard", "version": "0.0.1", "kibanaVersion": "kibana", + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "configPath": ["xpack", "licenseApiGuard"], "server": true, "ui": false diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json index be2e21c7eb41e..a06bfbb9409fc 100644 --- a/x-pack/plugins/license_management/kibana.json +++ b/x-pack/plugins/license_management/kibana.json @@ -3,13 +3,13 @@ "version": "kibana", "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["home", "licensing", "management", "features"], "optionalPlugins": ["telemetry"], "configPath": ["xpack", "license_management"], "extraPublicDirs": ["common/constants"], - "requiredBundles": [ - "telemetryManagementSection", - "esUiShared", - "kibanaReact" - ] + "requiredBundles": ["telemetryManagementSection", "esUiShared", "kibanaReact"] } diff --git a/x-pack/plugins/lists/kibana.json b/x-pack/plugins/lists/kibana.json index ae7b3e7679e0b..17a900b3f6fdc 100644 --- a/x-pack/plugins/lists/kibana.json +++ b/x-pack/plugins/lists/kibana.json @@ -2,6 +2,10 @@ "configPath": ["xpack", "lists"], "extraPublicDirs": ["common"], "id": "lists", + "owner": { + "name": "Security detections response", + "githubTeam": "security-detections-response" + }, "kibanaVersion": "kibana", "requiredPlugins": [], "optionalPlugins": ["spaces", "security"], diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json index 0d14312a154e0..2ff4aac9ba55b 100644 --- a/x-pack/plugins/logstash/kibana.json +++ b/x-pack/plugins/logstash/kibana.json @@ -1,18 +1,14 @@ { "id": "logstash", + "owner": { + "name": "Logstash", + "githubTeam": "logstash" + }, "version": "0.0.1", "kibanaVersion": "kibana", "configPath": ["xpack", "logstash"], - "requiredPlugins": [ - "licensing", - "management", - "features" - ], - "optionalPlugins": [ - "home", - "monitoring", - "security" - ], + "requiredPlugins": ["licensing", "management", "features"], + "optionalPlugins": ["home", "monitoring", "security"], "server": true, "ui": true, "requiredBundles": ["home"] diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 1cccfaa7748b1..e2cc415820db5 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -1,11 +1,12 @@ { "id": "maps", + "owner": { + "name": "GIS", + "githubTeam": "kibana-gis" + }, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "maps" - ], + "configPath": ["xpack", "maps"], "requiredPlugins": [ "licensing", "features", @@ -14,11 +15,11 @@ "fileUpload", "uiActions", "navigation", + "expressions", "visualizations", "dashboard", "embeddable", "mapsEms", - "usageCollection", "savedObjects", "share", "presentationUtil" @@ -27,7 +28,8 @@ "home", "savedObjectsTagging", "charts", - "security" + "security", + "usageCollection" ], "ui": true, "server": true, @@ -37,6 +39,7 @@ "requiredBundles": [ "kibanaReact", "kibanaUtils", + "usageCollection", "home", "mapsEms" ] diff --git a/x-pack/plugins/maps/public/_index.scss b/x-pack/plugins/maps/public/_index.scss index 01363209cfffd..e43ccb0e8679c 100644 --- a/x-pack/plugins/maps/public/_index.scss +++ b/x-pack/plugins/maps/public/_index.scss @@ -13,4 +13,5 @@ @import 'connected_components/index'; @import 'components/index'; @import 'classes/index'; -@import 'animations'; \ No newline at end of file +@import 'animations'; +@import 'embeddable/index'; \ No newline at end of file diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts index 658a093321500..229532c09f955 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts @@ -29,6 +29,19 @@ import { getJoinAggKey } from '../../../common/get_agg_key'; const defaultDynamicProperties = getDefaultDynamicProperties(); +export interface CreateRegionMapLayerDescriptorParams { + label: string; + emsLayerId?: string; + leftFieldName?: string; + termsFieldName?: string; + termsSize?: number; + colorSchema: string; + indexPatternId?: string; + indexPatternTitle?: string; + metricAgg: string; + metricFieldName?: string; +} + export function createAggDescriptor(metricAgg: string, metricFieldName?: string): AggDescriptor { const aggTypeKey = Object.keys(AGG_TYPE).find((key) => { return AGG_TYPE[key as keyof typeof AGG_TYPE] === metricAgg; @@ -55,18 +68,7 @@ export function createRegionMapLayerDescriptor({ indexPatternTitle, metricAgg, metricFieldName, -}: { - label: string; - emsLayerId?: string; - leftFieldName?: string; - termsFieldName?: string; - termsSize?: number; - colorSchema: string; - indexPatternId?: string; - indexPatternTitle?: string; - metricAgg: string; - metricFieldName?: string; -}): LayerDescriptor | null { +}: CreateRegionMapLayerDescriptorParams): LayerDescriptor | null { if (!indexPatternId || !emsLayerId || !leftFieldName || !termsFieldName) { return null; } diff --git a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts index e3e5f3878ee56..98217a5f28ad8 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts @@ -33,6 +33,16 @@ import { isMetricCountable } from '../util/is_metric_countable'; const defaultDynamicProperties = getDefaultDynamicProperties(); +export interface CreateTileMapLayerDescriptorParams { + label: string; + mapType: string; + colorSchema: string; + indexPatternId?: string; + geoFieldName?: string; + metricAgg: string; + metricFieldName?: string; +} + function isHeatmap(mapType: string): boolean { return mapType.toLowerCase() === 'heatmap'; } @@ -81,15 +91,7 @@ export function createTileMapLayerDescriptor({ geoFieldName, metricAgg, metricFieldName, -}: { - label: string; - mapType: string; - colorSchema: string; - indexPatternId?: string; - geoFieldName?: string; - metricAgg: string; - metricFieldName?: string; -}): LayerDescriptor | null { +}: CreateTileMapLayerDescriptorParams): LayerDescriptor | null { if (!indexPatternId || !geoFieldName) { return null; } diff --git a/x-pack/plugins/maps/public/connected_components/map_container/index.ts b/x-pack/plugins/maps/public/connected_components/map_container/index.ts index 4831cd60474b9..28671b13df5bc 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/index.ts +++ b/x-pack/plugins/maps/public/connected_components/map_container/index.ts @@ -19,7 +19,6 @@ import { getQueryableUniqueIndexPatternIds, } from '../../selectors/map_selectors'; import { MapStoreState } from '../../reducers/store'; -import { getCoreChrome } from '../../kibana_services'; function mapStateToProps(state: MapStoreState) { return { @@ -35,10 +34,7 @@ function mapStateToProps(state: MapStoreState) { function mapDispatchToProps(dispatch: ThunkDispatch) { return { - exitFullScreen: () => { - dispatch(exitFullScreen()); - getCoreChrome().setIsVisible(true); - }, + exitFullScreen: () => dispatch(exitFullScreen()), cancelAllInFlightRequests: () => dispatch(cancelAllInFlightRequests()), }; } diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 0bdf462cca4b3..2fad5d4eb64ac 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -21,6 +21,7 @@ import { ToolbarOverlay } from '../toolbar_overlay'; import { EditLayerPanel } from '../edit_layer_panel'; import { AddLayerPanel } from '../add_layer_panel'; import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public'; +import { getCoreChrome } from '../../kibana_services'; import { RawValue } from '../../../common/constants'; import { FLYOUT_STATE } from '../../reducers/ui'; import { MapSettings } from '../../reducers/map'; @@ -49,6 +50,12 @@ export interface Props { settings: MapSettings; layerList: ILayer[]; waitUntilTimeLayersLoad$: Observable; + /* + * Set to false to exclude sharing attributes 'data-*'. + * An example usage is tile_map and region_map visualizations. The visualizations use MapEmbeddable for rendering. + * Visualize Embeddable handles sharing attributes so sharing attributes are not needed in the children. + */ + isSharable: boolean; } interface State { @@ -79,7 +86,11 @@ export class MapContainer extends Component { componentDidUpdate() { this._loadShowFitToBoundsButton(); this._loadShowTimesliderButton(); - if (this.props.areLayersLoaded && !this._isInitalLoadRenderTimerStarted) { + if ( + this.props.isSharable && + this.props.areLayersLoaded && + !this._isInitalLoadRenderTimerStarted + ) { this._isInitalLoadRenderTimerStarted = true; this._startInitialLoadRenderTimer(); } @@ -190,18 +201,22 @@ export class MapContainer extends Component { let exitFullScreenButton; if (isFullScreen) { - exitFullScreenButton = ; + exitFullScreenButton = ( + + ); } + const shareAttributes = this.props.isSharable + ? { + ['data-dom-id']: this.state.domId, + ['data-render-complete']: this.state.isInitialLoadRenderTimeoutComplete, + ['data-shared-item']: true, + ['data-title']: this.props.title, + ['data-description']: this.props.description, + } + : {}; + return ( - + - {props.pointsOnly ? null : ( - <> - props.setDrawShape(DRAW_SHAPE.LINE)} - iconType={VectorLineIcon} - aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawLineLabel', { - defaultMessage: 'Draw line', - })} - title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawLineTitle', { - defaultMessage: 'Draw line', - })} - aria-pressed={drawLineSelected} - isSelected={drawLineSelected} - display={drawLineSelected ? 'fill' : 'empty'} - /> + + + {props.pointsOnly ? null : ( + <> + props.setDrawShape(DRAW_SHAPE.LINE)} + iconType={VectorLineIcon} + aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawLineLabel', { + defaultMessage: 'Draw line', + })} + title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawLineTitle', { + defaultMessage: 'Draw line', + })} + aria-pressed={drawLineSelected} + isSelected={drawLineSelected} + display={drawLineSelected ? 'fill' : 'empty'} + /> - props.setDrawShape(DRAW_SHAPE.POLYGON)} - iconType="node" - aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPolygonLabel', { - defaultMessage: 'Draw polygon', - })} - title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPolygonTitle', { - defaultMessage: 'Draw polygon', - })} - aria-pressed={drawPolygonSelected} - isSelected={drawPolygonSelected} - display={drawPolygonSelected ? 'fill' : 'empty'} - /> - props.setDrawShape(DRAW_SHAPE.DISTANCE)} - iconType={VectorCircleIcon} - aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawCircleLabel', { - defaultMessage: 'Draw circle', - })} - title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawCircleTitle', { - defaultMessage: 'Draw circle', - })} - aria-pressed={drawCircleSelected} - isSelected={drawCircleSelected} - display={drawCircleSelected ? 'fill' : 'empty'} - /> - props.setDrawShape(DRAW_SHAPE.BOUNDS)} - iconType={VectorSquareIcon} - aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawBBoxLabel', { - defaultMessage: 'Draw bounding box', - })} - title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawBBoxTitle', { - defaultMessage: 'Draw bounding box', - })} - aria-pressed={drawBBoxSelected} - isSelected={drawBBoxSelected} - display={drawBBoxSelected ? 'fill' : 'empty'} - /> - - )} - props.setDrawShape(DRAW_SHAPE.POINT)} - iconType="dot" - aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPointLabel', { - defaultMessage: 'Draw point', - })} - title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPointTitle', { - defaultMessage: 'Draw point', - })} - aria-pressed={drawPointSelected} - isSelected={drawPointSelected} - display={drawPointSelected ? 'fill' : 'empty'} - /> - props.setDrawShape(DRAW_SHAPE.DELETE)} - iconType="trash" - aria-label={i18n.translate( - 'xpack.maps.toolbarOverlay.featureDraw.deletePointOrShapeLabel', - { - defaultMessage: 'Delete point or shape', - } + props.setDrawShape(DRAW_SHAPE.POLYGON)} + iconType="node" + aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPolygonLabel', { + defaultMessage: 'Draw polygon', + })} + title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPolygonTitle', { + defaultMessage: 'Draw polygon', + })} + aria-pressed={drawPolygonSelected} + isSelected={drawPolygonSelected} + display={drawPolygonSelected ? 'fill' : 'empty'} + /> + props.setDrawShape(DRAW_SHAPE.DISTANCE)} + iconType={VectorCircleIcon} + aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawCircleLabel', { + defaultMessage: 'Draw circle', + })} + title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawCircleTitle', { + defaultMessage: 'Draw circle', + })} + aria-pressed={drawCircleSelected} + isSelected={drawCircleSelected} + display={drawCircleSelected ? 'fill' : 'empty'} + /> + props.setDrawShape(DRAW_SHAPE.BOUNDS)} + iconType={VectorSquareIcon} + aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawBBoxLabel', { + defaultMessage: 'Draw bounding box', + })} + title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawBBoxTitle', { + defaultMessage: 'Draw bounding box', + })} + aria-pressed={drawBBoxSelected} + isSelected={drawBBoxSelected} + display={drawBBoxSelected ? 'fill' : 'empty'} + /> + )} - title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.deletePointOrShapeTitle', { - defaultMessage: 'Delete point or shape', - })} - aria-pressed={deleteSelected} - isSelected={deleteSelected} - display={deleteSelected ? 'fill' : 'empty'} - /> - + props.setDrawShape(DRAW_SHAPE.POINT)} + iconType="dot" + aria-label={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPointLabel', { + defaultMessage: 'Draw point', + })} + title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.drawPointTitle', { + defaultMessage: 'Draw point', + })} + aria-pressed={drawPointSelected} + isSelected={drawPointSelected} + display={drawPointSelected ? 'fill' : 'empty'} + /> + props.setDrawShape(DRAW_SHAPE.DELETE)} + iconType="trash" + aria-label={i18n.translate( + 'xpack.maps.toolbarOverlay.featureDraw.deletePointOrShapeLabel', + { + defaultMessage: 'Delete point or shape', + } + )} + title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.deletePointOrShapeTitle', { + defaultMessage: 'Delete point or shape', + })} + aria-pressed={deleteSelected} + isSelected={deleteSelected} + display={deleteSelected ? 'fill' : 'empty'} + /> + + ); } diff --git a/x-pack/plugins/maps/public/embeddable/_index.scss b/x-pack/plugins/maps/public/embeddable/_index.scss new file mode 100644 index 0000000000000..966236f54d259 --- /dev/null +++ b/x-pack/plugins/maps/public/embeddable/_index.scss @@ -0,0 +1,8 @@ +.mapEmbeddableContainer { + width: 100%; + height: 100%; + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents +} \ No newline at end of file diff --git a/x-pack/plugins/maps/public/embeddable/map_component.tsx b/x-pack/plugins/maps/public/embeddable/map_component.tsx new file mode 100644 index 0000000000000..437dc13394316 --- /dev/null +++ b/x-pack/plugins/maps/public/embeddable/map_component.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component, RefObject } from 'react'; +import uuid from 'uuid/v4'; +import { EuiLoadingChart } from '@elastic/eui'; +import type { Filter, Query, TimeRange } from '../../../../../src/plugins/data/common'; +import type { LayerDescriptor, MapCenterAndZoom } from '../../common/descriptor_types'; +import type { MapEmbeddableType } from './types'; +import type { LazyLoadedMapModules } from '../lazy_load_bundle'; +import { lazyLoadMapModules } from '../lazy_load_bundle'; + +interface Props { + filters?: Filter[]; + query?: Query; + timeRange?: TimeRange; + getLayerDescriptors: ( + mapModules: Pick< + LazyLoadedMapModules, + 'createTileMapLayerDescriptor' | 'createRegionMapLayerDescriptor' + > + ) => LayerDescriptor[]; + mapCenter?: MapCenterAndZoom; + onInitialRenderComplete?: () => void; + /* + * Set to false to exclude sharing attributes 'data-*'. + */ + isSharable?: boolean; +} + +interface State { + isLoaded: boolean; +} + +export class MapComponent extends Component { + private _isMounted = false; + private _mapEmbeddable?: MapEmbeddableType | undefined; + private readonly _embeddableRef: RefObject = React.createRef(); + + state: State = { isLoaded: false }; + + componentDidMount() { + this._isMounted = true; + this._load(); + } + + componentWillUnmount() { + this._isMounted = false; + if (this._mapEmbeddable) { + this._mapEmbeddable.destroy(); + } + } + + componentDidUpdate() { + if (this._mapEmbeddable) { + this._mapEmbeddable.updateInput({ + filters: this.props.filters, + query: this.props.query, + timeRange: this.props.timeRange, + }); + } + } + + async _load() { + const mapModules = await lazyLoadMapModules(); + if (!this._isMounted) { + return; + } + + this.setState({ isLoaded: true }); + + this._mapEmbeddable = new mapModules.MapEmbeddable( + { + editable: false, + }, + { + id: uuid(), + attributes: { + title: '', + layerListJSON: JSON.stringify([ + mapModules.createBasemapLayerDescriptor(), + ...this.props.getLayerDescriptors({ + createRegionMapLayerDescriptor: mapModules.createRegionMapLayerDescriptor, + createTileMapLayerDescriptor: mapModules.createTileMapLayerDescriptor, + }), + ]), + }, + mapCenter: this.props.mapCenter, + } + ); + this._mapEmbeddable.setOnInitialRenderComplete(this.props.onInitialRenderComplete); + if (this.props.isSharable !== undefined) { + this._mapEmbeddable.setIsSharable(this.props.isSharable); + } + if (this._embeddableRef.current) { + this._mapEmbeddable.render(this._embeddableRef.current); + } + } + + render() { + if (!this.state.isLoaded) { + return ; + } + + return
; + } +} diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 642280aa9dc13..3255fb6b5e8ee 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -43,6 +43,7 @@ import { EventHandlers, } from '../reducers/non_serializable_instances'; import { + areLayersLoaded, getGeoFieldNames, getMapCenter, getMapBuffer, @@ -113,6 +114,9 @@ export class MapEmbeddable private _unsubscribeFromStore?: Unsubscribe; private _isInitialized = false; private _controlledBy: string; + private _onInitialRenderComplete?: () => void = undefined; + private _hasInitialRenderCompleteFired = false; + private _isSharable = true; constructor(config: MapEmbeddableConfig, initialInput: MapEmbeddableInput, parent?: IContainer) { super( @@ -231,6 +235,17 @@ export class MapEmbeddable this._savedMap.getStore().dispatch(setEventHandlers(eventHandlers)); }; + public setOnInitialRenderComplete(onInitialRenderComplete?: () => void): void { + this._onInitialRenderComplete = onInitialRenderComplete; + } + + /* + * Set to false to exclude sharing attributes 'data-*'. + */ + public setIsSharable(isSharable: boolean): void { + this._isSharable = isSharable; + } + getInspectorAdapters() { return getInspectorAdapters(this._savedMap.getStore().getState()); } @@ -351,6 +366,7 @@ export class MapEmbeddable title={this.getTitle()} description={this.getDescription()} waitUntilTimeLayersLoad$={waitUntilTimeLayersLoad$(this._savedMap.getStore())} + isSharable={this._isSharable} /> , @@ -510,6 +526,15 @@ export class MapEmbeddable return; } + if ( + this._onInitialRenderComplete && + !this._hasInitialRenderCompleteFired && + areLayersLoaded(this._savedMap.getStore().getState()) + ) { + this._hasInitialRenderCompleteFired = true; + this._onInitialRenderComplete(); + } + const mapExtent = getMapExtent(this._savedMap.getStore().getState()); if (this.input.filterByMapExtent && !_.isEqual(this._prevMapExtent, mapExtent)) { this.setMapExtentFilter(); diff --git a/x-pack/plugins/maps/public/embeddable/types.ts b/x-pack/plugins/maps/public/embeddable/types.ts index fd8160c567530..090a6c8d02043 100644 --- a/x-pack/plugins/maps/public/embeddable/types.ts +++ b/x-pack/plugins/maps/public/embeddable/types.ts @@ -7,6 +7,7 @@ import type { IndexPattern } from '../../../../../src/plugins/data/common/index_patterns'; import { + Embeddable, EmbeddableInput, EmbeddableOutput, SavedObjectEmbeddableInput, @@ -43,3 +44,8 @@ export type MapEmbeddableInput = MapByValueInput | MapByReferenceInput; export type MapEmbeddableOutput = EmbeddableOutput & { indexPatterns: IndexPattern[]; }; + +export type MapEmbeddableType = Embeddable & { + setOnInitialRenderComplete(onInitialRenderComplete?: () => void): void; + setIsSharable(isSharable: boolean): void; +}; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 3e5e2d54422d6..788e5938ee168 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -8,25 +8,31 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IndexPatternsContract } from 'src/plugins/data/public/index_patterns'; import { AppMountParameters } from 'kibana/public'; -import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public'; +import { IContainer } from '../../../../../src/plugins/embeddable/public'; import { LayerDescriptor } from '../../common/descriptor_types'; -import { MapEmbeddableConfig, MapEmbeddableInput, MapEmbeddableOutput } from '../embeddable/types'; +import type { + MapEmbeddableConfig, + MapEmbeddableInput, + MapEmbeddableType, +} from '../embeddable/types'; import { SourceRegistryEntry } from '../classes/sources/source_registry'; import { LayerWizard } from '../classes/layers/layer_wizard_registry'; import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source'; import type { EMSTermJoinConfig, SampleValuesConfig } from '../ems_autosuggest'; +import type { CreateTileMapLayerDescriptorParams } from '../classes/layers/create_tile_map_layer_descriptor'; +import type { CreateRegionMapLayerDescriptorParams } from '../classes/layers/create_region_map_layer_descriptor'; let loadModulesPromise: Promise; -interface LazyLoadedMapModules { +export interface LazyLoadedMapModules { MapEmbeddable: new ( config: MapEmbeddableConfig, initialInput: MapEmbeddableInput, parent?: IContainer - ) => Embeddable; + ) => MapEmbeddableType; getIndexPatternService: () => IndexPatternsContract; getMapsCapabilities: () => any; - renderApp: (params: AppMountParameters) => Promise<() => void>; + renderApp: (params: AppMountParameters, AppUsageTracker: React.FC) => Promise<() => void>; createSecurityLayerDescriptors: ( indexPatternId: string, indexPatternTitle: string @@ -41,15 +47,7 @@ interface LazyLoadedMapModules { geoFieldName, metricAgg, metricFieldName, - }: { - label: string; - mapType: string; - colorSchema: string; - indexPatternId?: string; - geoFieldName?: string; - metricAgg: string; - metricFieldName?: string; - }) => LayerDescriptor | null; + }: CreateTileMapLayerDescriptorParams) => LayerDescriptor | null; createRegionMapLayerDescriptor: ({ label, emsLayerId, @@ -61,18 +59,7 @@ interface LazyLoadedMapModules { indexPatternTitle, metricAgg, metricFieldName, - }: { - label: string; - emsLayerId?: string; - leftFieldName?: string; - termsFieldName?: string; - termsSize?: number; - colorSchema: string; - indexPatternId?: string; - indexPatternTitle?: string; - metricAgg: string; - metricFieldName?: string; - }) => LayerDescriptor | null; + }: CreateRegionMapLayerDescriptorParams) => LayerDescriptor | null; createBasemapLayerDescriptor: () => LayerDescriptor | null; createESSearchSourceLayerDescriptor: (params: CreateLayerDescriptorParams) => LayerDescriptor; suggestEMSTermJoinConfig: (config: SampleValuesConfig) => Promise; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/index.ts b/x-pack/plugins/maps/public/legacy_visualizations/index.ts new file mode 100644 index 0000000000000..a01e0ebefb5ad --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { createRegionMapFn, regionMapRenderer, regionMapVisType } from './region_map'; +export { createTileMapFn, tileMapRenderer, tileMapVisType } from './tile_map'; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/index.ts b/x-pack/plugins/maps/public/legacy_visualizations/region_map/index.ts new file mode 100644 index 0000000000000..cda57b1088793 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { regionMapVisType } from './region_map_vis_type'; +export { createRegionMapFn } from './region_map_fn'; +export { regionMapRenderer } from './region_map_renderer'; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_editor.tsx b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_editor.tsx new file mode 100644 index 0000000000000..8830c557f7b4a --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_editor.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { Vis } from '../../../../../../src/plugins/visualizations/public'; +import { getData, getShareService } from '../../kibana_services'; +import { ViewInMaps } from '../view_in_maps'; +import { extractLayerDescriptorParams } from './utils'; +import { RegionMapVisParams } from './types'; +import { title } from './region_map_vis_type'; + +export function RegionMapEditor(props: VisEditorOptionsProps) { + const onClick = (e: React.MouseEvent) => { + e.preventDefault(); + + const locator = getShareService().url.locators.get('MAPS_APP_REGION_MAP_LOCATOR'); + if (!locator) return; + + const query = getData().query; + locator.navigate({ + ...extractLayerDescriptorParams((props.vis as unknown) as Vis), + filters: query.filterManager.getFilters(), + query: query.queryString.getQuery(), + timeRange: query.timefilter.timefilter.getTime(), + }); + }; + + return ; +} diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_fn.ts b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_fn.ts new file mode 100644 index 0000000000000..f5329aa4212bf --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_fn.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { Filter, Query, TimeRange } from '../../../../../../src/plugins/data/common'; +import type { ExpressionValueSearchContext } from '../../../../../../src/plugins/data/common/search/expressions/kibana_context_type'; +import type { + ExpressionFunctionDefinition, + Render, +} from '../../../../../../src/plugins/expressions/public'; +import { REGION_MAP_RENDER, REGION_MAP_VIS_TYPE, RegionMapVisConfig } from './types'; + +interface Arguments { + visConfig: string; +} + +export interface RegionMapVisRenderValue { + visType: typeof REGION_MAP_VIS_TYPE; + visConfig: RegionMapVisConfig; + filters?: Filter[]; + query?: Query; + timeRange?: TimeRange; +} + +export type RegionMapExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'regionmap', + ExpressionValueSearchContext, + Arguments, + Promise> +>; + +export const createRegionMapFn = (): RegionMapExpressionFunctionDefinition => ({ + name: 'regionmap', + type: 'render', + help: i18n.translate('xpack.maps.regionMap.function.help', { + defaultMessage: 'Regionmap visualization', + }), + args: { + visConfig: { + types: ['string'], + default: '"{}"', + help: '', + }, + }, + async fn(input, args) { + return { + type: 'render', + as: REGION_MAP_RENDER, + value: { + visType: REGION_MAP_VIS_TYPE, + visConfig: JSON.parse(args.visConfig), + filters: input.filters, + query: Array.isArray(input.query) ? input.query[0] : input.query, + timeRange: input.timeRange, + }, + }; + }, +}); diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_renderer.tsx b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_renderer.tsx new file mode 100644 index 0000000000000..1d3531bfed82a --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_renderer.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import type { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { RegionMapVisRenderValue } from './region_map_fn'; +import { RegionMapVisualization } from './region_map_visualization'; +import { REGION_MAP_RENDER } from './types'; + +export const regionMapRenderer = { + name: REGION_MAP_RENDER, + reuseDomNode: true, + render: async (domNode, { filters, query, timeRange, visConfig }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + render( + { + handlers.done(); + }} + filters={filters} + query={query} + timeRange={timeRange} + visConfig={visConfig} + />, + domNode + ); + }, +} as ExpressionRenderDefinition; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.ts b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.ts new file mode 100644 index 0000000000000..4c6e4b2150fb1 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { VisTypeDefinition } from '../../../../../../src/plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { REGION_MAP_VIS_TYPE, RegionMapVisParams } from './types'; +import { RegionMapEditor } from './region_map_editor'; + +export const title = i18n.translate('xpack.maps.regionMapMap.vis.title', { + defaultMessage: 'Region Map', +}); + +export const regionMapVisType = { + name: REGION_MAP_VIS_TYPE, + title, + icon: 'visMapRegion', + description: i18n.translate('xpack.maps.regionMap.vis.description', { + defaultMessage: 'Show metrics on a thematic map.', + }), + editorConfig: { + optionTabs: [ + { + name: '', + title: '', + editor: RegionMapEditor, + }, + ], + }, + visConfig: { + defaults: { + colorSchema: 'Yellow to Red', + mapZoom: 2, + mapCenter: [0, 0], + }, + }, + toExpressionAst, + requiresSearch: true, +} as VisTypeDefinition; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx new file mode 100644 index 0000000000000..5bb75d781e79b --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Filter, Query, TimeRange } from '../../../../../../src/plugins/data/common'; +import { RegionMapVisConfig } from './types'; +import type { LazyLoadedMapModules } from '../../lazy_load_bundle'; +import { MapComponent } from '../../embeddable/map_component'; + +interface Props { + filters?: Filter[]; + query?: Query; + timeRange?: TimeRange; + visConfig: RegionMapVisConfig; + onInitialRenderComplete: () => void; +} + +export function RegionMapVisualization(props: Props) { + const mapCenter = { + lat: props.visConfig.mapCenter[0], + lon: props.visConfig.mapCenter[1], + zoom: props.visConfig.mapZoom, + }; + function getLayerDescriptors({ + createRegionMapLayerDescriptor, + }: { + createRegionMapLayerDescriptor: LazyLoadedMapModules['createRegionMapLayerDescriptor']; + }) { + const layerDescriptor = createRegionMapLayerDescriptor(props.visConfig.layerDescriptorParams); + return layerDescriptor ? [layerDescriptor] : []; + } + return ( + + ); +} diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/to_ast.ts b/x-pack/plugins/maps/public/legacy_visualizations/region_map/to_ast.ts new file mode 100644 index 0000000000000..49f431980b950 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/to_ast.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + buildExpression, + buildExpressionFunction, +} from '../../../../../../src/plugins/expressions/public'; +import { VisToExpressionAst } from '../../../../../../src/plugins/visualizations/public'; +import { RegionMapExpressionFunctionDefinition } from './region_map_fn'; +import { RegionMapVisParams } from './types'; +import { extractLayerDescriptorParams } from './utils'; + +export const toExpressionAst: VisToExpressionAst = (vis) => { + const regionMap = buildExpressionFunction('regionmap', { + visConfig: JSON.stringify({ + ...vis.params, + mapCenter: vis.uiState.get('mapCenter', [0, 0]), + mapZoom: parseInt(vis.uiState.get('mapZoom', 2), 10), + layerDescriptorParams: extractLayerDescriptorParams(vis), + }), + }); + + const ast = buildExpression([regionMap]); + + return ast.toAst(); +}; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/types.ts b/x-pack/plugins/maps/public/legacy_visualizations/region_map/types.ts new file mode 100644 index 0000000000000..1ed725798ee58 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CreateRegionMapLayerDescriptorParams } from '../../classes/layers/create_region_map_layer_descriptor'; + +export const REGION_MAP_RENDER = 'region_map_vis'; +export const REGION_MAP_VIS_TYPE = 'region_map'; + +export interface RegionMapVisParams { + colorSchema: string; + mapZoom: number; + mapCenter: [number, number]; + selectedLayer: { + isEMS: boolean; + id: string | number; + layerId: string; + }; + selectedJoinField: { + name: string; + }; +} + +export interface RegionMapVisConfig extends RegionMapVisParams { + layerDescriptorParams: CreateRegionMapLayerDescriptorParams; +} diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/utils.ts b/x-pack/plugins/maps/public/legacy_visualizations/region_map/utils.ts new file mode 100644 index 0000000000000..a641bbb5a7c39 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/utils.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Vis } from '../../../../../../src/plugins/visualizations/public'; +import { RegionMapVisParams } from './types'; +import { title } from './region_map_vis_type'; + +function getEmsLayerId(id: string | number, layerId: string) { + if (typeof id === 'string') { + return id; + } + + // Region maps from 6.x will have numerical EMS id refering to S3 bucket id. + // In this case, use layerId with contains the EMS layer name. + const split = layerId.split('.'); + return split.length === 2 ? split[1] : undefined; +} + +export function extractLayerDescriptorParams(vis: Vis) { + const params: { [key: string]: any } = { + label: vis.title ? vis.title : title, + emsLayerId: vis.params.selectedLayer.isEMS + ? getEmsLayerId(vis.params.selectedLayer.id, vis.params.selectedLayer.layerId) + : undefined, + leftFieldName: vis.params.selectedLayer.isEMS ? vis.params.selectedJoinField.name : undefined, + colorSchema: vis.params.colorSchema, + indexPatternId: vis.data.indexPattern?.id, + indexPatternTitle: vis.data.indexPattern?.title, + metricAgg: 'count', + }; + + const bucketAggs = vis.data?.aggs?.byType('buckets'); + if (bucketAggs?.length && bucketAggs[0].type.dslName === 'terms') { + params.termsFieldName = bucketAggs[0].getField()?.name; + params.termsSize = bucketAggs[0].getParam('size'); + } + + const metricAggs = vis.data?.aggs?.byType('metrics'); + if (metricAggs?.length) { + params.metricAgg = metricAggs[0].type.dslName; + params.metricFieldName = metricAggs[0].getField()?.name; + } + + return params; +} diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/index.ts b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/index.ts new file mode 100644 index 0000000000000..04d4c160fb510 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { tileMapVisType } from './tile_map_vis_type'; +export { createTileMapFn } from './tile_map_fn'; +export { tileMapRenderer } from './tile_map_renderer'; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_editor.tsx b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_editor.tsx new file mode 100644 index 0000000000000..b177b34a537f9 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_editor.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { Vis } from '../../../../../../src/plugins/visualizations/public'; +import { getData, getShareService } from '../../kibana_services'; +import { ViewInMaps } from '../view_in_maps'; +import { extractLayerDescriptorParams } from './utils'; +import { TileMapVisParams } from './types'; +import { title } from './tile_map_vis_type'; + +export function TileMapEditor(props: VisEditorOptionsProps) { + const onClick = (e: React.MouseEvent) => { + e.preventDefault(); + + const locator = getShareService().url.locators.get('MAPS_APP_TILE_MAP_LOCATOR'); + if (!locator) return; + + const query = getData().query; + locator.navigate({ + ...extractLayerDescriptorParams((props.vis as unknown) as Vis), + filters: query.filterManager.getFilters(), + query: query.queryString.getQuery(), + timeRange: query.timefilter.timefilter.getTime(), + }); + }; + + return ; +} diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_fn.ts b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_fn.ts new file mode 100644 index 0000000000000..96c962be0c31b --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_fn.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { Filter, Query, TimeRange } from '../../../../../../src/plugins/data/common'; +import type { ExpressionValueSearchContext } from '../../../../../../src/plugins/data/common/search/expressions/kibana_context_type'; +import type { + ExpressionFunctionDefinition, + Render, +} from '../../../../../../src/plugins/expressions/public'; +import { TILE_MAP_RENDER, TILE_MAP_VIS_TYPE, TileMapVisConfig } from './types'; + +interface Arguments { + visConfig: string; +} + +export interface TileMapVisRenderValue { + visType: typeof TILE_MAP_VIS_TYPE; + visConfig: TileMapVisConfig; + filters?: Filter[]; + query?: Query; + timeRange?: TimeRange; +} + +export type TileMapExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'tilemap', + ExpressionValueSearchContext, + Arguments, + Promise> +>; + +export const createTileMapFn = (): TileMapExpressionFunctionDefinition => ({ + name: 'tilemap', + type: 'render', + help: i18n.translate('xpack.maps.tileMap.function.help', { + defaultMessage: 'Tilemap visualization', + }), + args: { + visConfig: { + types: ['string'], + default: '"{}"', + help: '', + }, + }, + async fn(input, args) { + return { + type: 'render', + as: TILE_MAP_RENDER, + value: { + visType: TILE_MAP_VIS_TYPE, + visConfig: JSON.parse(args.visConfig), + filters: input.filters, + query: Array.isArray(input.query) ? input.query[0] : input.query, + timeRange: input.timeRange, + }, + }; + }, +}); diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_renderer.tsx b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_renderer.tsx new file mode 100644 index 0000000000000..5e61a0e0cd368 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_renderer.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import type { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { TileMapVisRenderValue } from './tile_map_fn'; +import { TileMapVisualization } from './tile_map_visualization'; +import { TILE_MAP_RENDER } from './types'; + +export const tileMapRenderer = { + name: TILE_MAP_RENDER, + reuseDomNode: true, + render: async (domNode, { filters, query, timeRange, visConfig }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + render( + { + handlers.done(); + }} + filters={filters} + query={query} + timeRange={timeRange} + visConfig={visConfig} + />, + domNode + ); + }, +} as ExpressionRenderDefinition; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.ts b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.ts new file mode 100644 index 0000000000000..458adcab8c8d1 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { VisTypeDefinition } from '../../../../../../src/plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { MapTypes, TileMapVisParams, TILE_MAP_VIS_TYPE } from './types'; +import { TileMapEditor } from './tile_map_editor'; + +export const title = i18n.translate('xpack.maps.tileMap.vis.title', { + defaultMessage: 'Coordinate Map', +}); + +export const tileMapVisType = { + name: TILE_MAP_VIS_TYPE, + title, + icon: 'visMapCoordinate', + description: i18n.translate('xpack.maps.tileMap.vis.description', { + defaultMessage: 'Plot latitude and longitude coordinates on a map', + }), + editorConfig: { + optionTabs: [ + { + name: '', + title: '', + editor: TileMapEditor, + }, + ], + }, + visConfig: { + defaults: { + colorSchema: 'Yellow to Red', + mapType: MapTypes.ScaledCircleMarkers, + mapZoom: 2, + mapCenter: [0, 0], + }, + }, + toExpressionAst, + requiresSearch: true, +} as VisTypeDefinition; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx new file mode 100644 index 0000000000000..225b29de5652b --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Filter, Query, TimeRange } from '../../../../../../src/plugins/data/common'; +import { TileMapVisConfig } from './types'; +import type { LazyLoadedMapModules } from '../../lazy_load_bundle'; +import { MapComponent } from '../../embeddable/map_component'; + +interface Props { + filters?: Filter[]; + query?: Query; + timeRange?: TimeRange; + visConfig: TileMapVisConfig; + onInitialRenderComplete: () => void; +} + +export function TileMapVisualization(props: Props) { + const mapCenter = { + lat: props.visConfig.mapCenter[0], + lon: props.visConfig.mapCenter[1], + zoom: props.visConfig.mapZoom, + }; + function getLayerDescriptors({ + createTileMapLayerDescriptor, + }: { + createTileMapLayerDescriptor: LazyLoadedMapModules['createTileMapLayerDescriptor']; + }) { + const layerDescriptor = createTileMapLayerDescriptor(props.visConfig.layerDescriptorParams); + return layerDescriptor ? [layerDescriptor] : []; + } + return ( + + ); +} diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/to_ast.ts b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/to_ast.ts new file mode 100644 index 0000000000000..5417dabfee8d4 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/to_ast.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + buildExpression, + buildExpressionFunction, +} from '../../../../../../src/plugins/expressions/public'; +import { VisToExpressionAst } from '../../../../../../src/plugins/visualizations/public'; +import { TileMapExpressionFunctionDefinition } from './tile_map_fn'; +import { TileMapVisParams } from './types'; +import { extractLayerDescriptorParams } from './utils'; + +export const toExpressionAst: VisToExpressionAst = (vis) => { + const tileMap = buildExpressionFunction('tilemap', { + visConfig: JSON.stringify({ + ...vis.params, + mapCenter: vis.uiState.get('mapCenter', [0, 0]), + mapZoom: parseInt(vis.uiState.get('mapZoom', 2), 10), + layerDescriptorParams: extractLayerDescriptorParams(vis), + }), + }); + + const ast = buildExpression([tileMap]); + + return ast.toAst(); +}; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/types.ts b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/types.ts new file mode 100644 index 0000000000000..4e65fb82b797d --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CreateTileMapLayerDescriptorParams } from '../../classes/layers/create_tile_map_layer_descriptor'; + +export const TILE_MAP_RENDER = 'tile_map_vis'; +export const TILE_MAP_VIS_TYPE = 'tile_map'; + +export enum MapTypes { + ScaledCircleMarkers = 'Scaled Circle Markers', + ShadedCircleMarkers = 'Shaded Circle Markers', + ShadedGeohashGrid = 'Shaded Geohash Grid', + Heatmap = 'Heatmap', +} + +export interface TileMapVisParams { + colorSchema: string; + mapType: MapTypes; + mapZoom: number; + mapCenter: [number, number]; +} + +export interface TileMapVisConfig extends TileMapVisParams { + layerDescriptorParams: CreateTileMapLayerDescriptorParams; +} diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/utils.ts b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/utils.ts new file mode 100644 index 0000000000000..3fcb3d8915701 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/utils.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Vis } from '../../../../../../src/plugins/visualizations/public'; +import { indexPatterns } from '../../../../../../src/plugins/data/public'; +import { TileMapVisParams } from './types'; +import { title } from './tile_map_vis_type'; + +export function extractLayerDescriptorParams(vis: Vis) { + const params: { [key: string]: any } = { + label: vis.title ? vis.title : title, + mapType: vis.params.mapType, + colorSchema: vis.params.colorSchema, + indexPatternId: vis.data.indexPattern?.id, + metricAgg: 'count', + }; + + const bucketAggs = vis.data?.aggs?.byType('buckets'); + if (bucketAggs?.length && bucketAggs[0].type.dslName === 'geohash_grid') { + params.geoFieldName = bucketAggs[0].getField()?.name; + } else if (vis.data.indexPattern) { + // attempt to default to first geo point field when geohash is not configured yet + const geoField = vis.data.indexPattern.fields.find((field) => { + return ( + !indexPatterns.isNestedField(field) && field.aggregatable && field.type === 'geo_point' + ); + }); + if (geoField) { + params.geoFieldName = geoField.name; + } + } + + const metricAggs = vis.data?.aggs?.byType('metrics'); + if (metricAggs?.length) { + params.metricAgg = metricAggs[0].type.dslName; + params.metricFieldName = metricAggs[0].getField()?.name; + } + + return params; +} diff --git a/x-pack/plugins/maps/public/legacy_visualizations/view_in_maps.tsx b/x-pack/plugins/maps/public/legacy_visualizations/view_in_maps.tsx new file mode 100644 index 0000000000000..39d959865a9a0 --- /dev/null +++ b/x-pack/plugins/maps/public/legacy_visualizations/view_in_maps.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +interface Props { + onClick: (e: React.MouseEvent) => void; + visualizationLabel: string; +} + +export function ViewInMaps(props: Props) { + return ( + +

+ +

+

+ +

+
+ + + +
+
+ ); +} diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 7ee84eb8b67e2..4ee2a83589c95 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -5,11 +5,13 @@ * 2.0. */ +import React from 'react'; import type { Setup as InspectorSetupContract } from 'src/plugins/inspector/public'; import type { UiActionsStart } from 'src/plugins/ui_actions/public'; import type { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import type { DashboardStart } from 'src/plugins/dashboard/public'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import type { AppMountParameters, CoreSetup, @@ -34,6 +36,7 @@ import type { VisualizationsSetup, VisualizationsStart, } from '../../../../src/plugins/visualizations/public'; +import type { Plugin as ExpressionsPublicPlugin } from '../../../../src/plugins/expressions/public'; import { APP_ICON_SOLUTION, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { VISUALIZE_GEO_FIELD_TRIGGER } from '../../../../src/plugins/ui_actions/public'; import { visualizeGeoFieldAction } from './trigger_actions/visualize_geo_field_action'; @@ -71,9 +74,18 @@ import { MapsAppRegionMapLocatorDefinition, MapsAppTileMapLocatorDefinition, } from './locators'; +import { + createRegionMapFn, + regionMapRenderer, + regionMapVisType, + createTileMapFn, + tileMapRenderer, + tileMapVisType, +} from './legacy_visualizations'; import { SecurityPluginStart } from '../../security/public'; export interface MapsPluginSetupDependencies { + expressions: ReturnType; inspector: InspectorSetupContract; home?: HomePublicPluginSetup; visualizations: VisualizationsSetup; @@ -81,6 +93,7 @@ export interface MapsPluginSetupDependencies { mapsEms: MapsEmsPluginSetup; share: SharePluginSetup; licensing: LicensingPluginSetup; + usageCollection?: UsageCollectionSetup; } export interface MapsPluginStartDependencies { @@ -168,10 +181,20 @@ export class MapsPlugin euiIconType: APP_ICON_SOLUTION, category: DEFAULT_APP_CATEGORIES.kibana, async mount(params: AppMountParameters) { + const UsageTracker = + plugins.usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; const { renderApp } = await lazyLoadMapModules(); - return renderApp(params); + return renderApp(params, UsageTracker); }, }); + + // register wrapper around legacy tile_map and region_map visualizations + plugins.expressions.registerFunction(createRegionMapFn); + plugins.expressions.registerRenderer(regionMapRenderer); + plugins.visualizations.createBaseVisualization(regionMapVisType); + plugins.expressions.registerFunction(createTileMapFn); + plugins.expressions.registerRenderer(tileMapRenderer); + plugins.visualizations.createBaseVisualization(tileMapVisType); } public start(core: CoreStart, plugins: MapsPluginStartDependencies): MapsStartApi { diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index 4d1dff9303b0c..c3f13e70fbd5c 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import _ from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Router, Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom'; @@ -62,12 +61,10 @@ function setAppChrome() { }); } -export async function renderApp({ - element, - history, - onAppLeave, - setHeaderActionMenu, -}: AppMountParameters) { +export async function renderApp( + { element, history, onAppLeave, setHeaderActionMenu }: AppMountParameters, + AppUsageTracker: React.FC +) { goToSpecifiedPath = (path) => history.push(path); kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, @@ -107,29 +104,31 @@ export async function renderApp({ const I18nContext = getCoreI18n().Context; render( - - - - - - // Redirect other routes to list, or if hash-containing, their non-hash equivalents - { - if (hash) { - // Remove leading hash - const newPath = hash.substr(1); - return ; - } else if (pathname === '/' || pathname === '') { - return ; - } else { - return ; - } - }} - /> - - - , + + + + + + + // Redirect other routes to list, or if hash-containing, their non-hash equivalents + { + if (hash) { + // Remove leading hash + const newPath = hash.substr(1); + return ; + } else if (pathname === '/' || pathname === '') { + return ; + } else { + return ; + } + }} + /> + + + + , element ); diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 5231aab5d1194..4a2b0fbefad68 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -456,6 +456,7 @@ export class MapApp extends React.Component { title={this.props.savedMap.getAttributes().title} description={this.props.savedMap.getAttributes().description} waitUntilTimeLayersLoad$={waitUntilTimeLayersLoad$(this.props.savedMap.getStore())} + isSharable />
diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 79bc820d67b46..6a49dd7ba7efe 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { Adapters } from 'src/plugins/inspector/public'; import { - getCoreChrome, getMapsCapabilities, getIsAllowByValueEmbeddables, getInspector, @@ -92,7 +91,6 @@ export function getTopNavConfig({ }), testId: 'mapsFullScreenMode', run() { - getCoreChrome().setIsVisible(false); enableFullScreen(); }, } diff --git a/x-pack/plugins/metrics_entities/kibana.json b/x-pack/plugins/metrics_entities/kibana.json index 17484c2c243ce..9d3a4f7f66a8d 100644 --- a/x-pack/plugins/metrics_entities/kibana.json +++ b/x-pack/plugins/metrics_entities/kibana.json @@ -1,5 +1,9 @@ { "id": "metricsEntities", + "owner": { + "name": "Security solution", + "githubTeam": "security-solution" + }, "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "metricsEntities"], diff --git a/x-pack/plugins/ml/common/constants/alerts.ts b/x-pack/plugins/ml/common/constants/alerts.ts index 2192b2b504b59..1b373b2ec435b 100644 --- a/x-pack/plugins/ml/common/constants/alerts.ts +++ b/x-pack/plugins/ml/common/constants/alerts.ts @@ -54,12 +54,12 @@ export const HEALTH_CHECK_NAMES: Record = { + 'cluster:admin/xpack/ml/job/delete': JOB_ACTION.DELETE, + 'cluster:admin/xpack/ml/job/reset': JOB_ACTION.RESET, + 'cluster:admin/xpack/ml/job/model_snapshots/revert': JOB_ACTION.REVERT, +}; + +export const JOB_ACTION_TASKS = Object.keys(JOB_ACTION_TASK); diff --git a/x-pack/plugins/ml/common/constants/jobs_list.ts b/x-pack/plugins/ml/common/constants/jobs_list.ts index 7672731d2a8e5..4667177890623 100644 --- a/x-pack/plugins/ml/common/constants/jobs_list.ts +++ b/x-pack/plugins/ml/common/constants/jobs_list.ts @@ -8,4 +8,5 @@ export const DEFAULT_REFRESH_INTERVAL_MS = 30000; export const MINIMUM_REFRESH_INTERVAL_MS = 1000; export const DELETING_JOBS_REFRESH_INTERVAL_MS = 2000; +export const RESETTING_JOBS_REFRESH_INTERVAL_MS = 1000; export const PROGRESS_JOBS_REFRESH_INTERVAL_MS = 2000; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts index 2ef1d824180ad..dcf18b98e00a0 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts @@ -10,8 +10,11 @@ import { estypes } from '@elastic/elasticsearch'; export type JobId = string; export type BucketSpan = string; +// temporary Job override, waiting for es client to have correct types export type Job = estypes.MlJob; +export type MlJobBlocked = estypes.MlJobBlocked; + export type AnalysisConfig = estypes.MlAnalysisConfig; export type Detector = estypes.MlDetector; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index 37dad58bfbd45..1a70c27faaf29 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -7,10 +7,11 @@ import { Moment } from 'moment'; -import { CombinedJob, CombinedJobWithStats } from './combined_job'; -import { MlAnomalyDetectionAlertRule } from '../alerts'; -export { Datafeed } from './datafeed'; -export { DatafeedStats } from './datafeed_stats'; +import type { CombinedJob, CombinedJobWithStats } from './combined_job'; +import type { MlAnomalyDetectionAlertRule } from '../alerts'; +import type { MlJobBlocked } from './job'; +export type { Datafeed } from './datafeed'; +export type { DatafeedStats } from './datafeed_stats'; export interface MlSummaryJob { id: string; @@ -31,7 +32,7 @@ export interface MlSummaryJob { auditMessage?: Partial; isSingleMetricViewerJob: boolean; isNotSingleMetricViewerJobMessage?: string; - deleting?: boolean; + blocked?: MlJobBlocked; latestTimestampSortValue?: number; earliestStartTimestampMs?: number; awaitingNodeAssignment: boolean; diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index ef8d35e52a951..306c42301e43a 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -40,6 +40,7 @@ export const adminMlCapabilities = { canDeleteJob: false, canOpenJob: false, canCloseJob: false, + canResetJob: false, canUpdateJob: false, canForecastJob: false, canCreateDatafeed: false, diff --git a/x-pack/plugins/ml/common/types/job_service.ts b/x-pack/plugins/ml/common/types/job_service.ts index e846635ee5380..a3e1571070ffd 100644 --- a/x-pack/plugins/ml/common/types/job_service.ts +++ b/x-pack/plugins/ml/common/types/job_service.ts @@ -48,3 +48,11 @@ export interface BulkCreateResults { datafeed: { success: boolean; error?: ErrorType }; }; } + +export interface ResetJobsResponse { + [jobId: string]: { + reset: boolean; + task?: string; + error?: ErrorType; + }; +} diff --git a/x-pack/plugins/ml/common/util/alerts.test.ts b/x-pack/plugins/ml/common/util/alerts.test.ts index 84205e6806133..65eca44e245a1 100644 --- a/x-pack/plugins/ml/common/util/alerts.test.ts +++ b/x-pack/plugins/ml/common/util/alerts.test.ts @@ -95,6 +95,9 @@ describe('getResultJobsHealthRuleConfig', () => { enabled: true, timeInterval: null, }, + errorMessages: { + enabled: true, + }, }); }); test('returns config with overridden values based on provided configuration', () => { @@ -119,6 +122,9 @@ describe('getResultJobsHealthRuleConfig', () => { enabled: true, timeInterval: null, }, + errorMessages: { + enabled: true, + }, }); }); }); diff --git a/x-pack/plugins/ml/common/util/alerts.ts b/x-pack/plugins/ml/common/util/alerts.ts index 7328c2a4dcc71..6abc5333a1f73 100644 --- a/x-pack/plugins/ml/common/util/alerts.ts +++ b/x-pack/plugins/ml/common/util/alerts.ts @@ -54,7 +54,7 @@ export function getTopNBuckets(job: Job): number { return Math.ceil(narrowBucketLength / bucketSpan.asSeconds()); } -const implementedTests = ['datafeed', 'mml', 'delayedData'] as JobsHealthTests[]; +const implementedTests = ['datafeed', 'mml', 'delayedData', 'errorMessages'] as JobsHealthTests[]; /** * Returns tests configuration combined with default values. diff --git a/x-pack/plugins/ml/common/util/errors/types.ts b/x-pack/plugins/ml/common/util/errors/types.ts index 3110f09e441cd..23d46ad4a8589 100644 --- a/x-pack/plugins/ml/common/util/errors/types.ts +++ b/x-pack/plugins/ml/common/util/errors/types.ts @@ -73,5 +73,5 @@ export function isMLResponseError(error: any): error is MLResponseError { } export function isBoomError(error: any): error is Boom.Boom { - return error.isBoom === true; + return error?.isBoom === true; } diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts index f6446b454a877..a5f433bcc3752 100644 --- a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts @@ -21,7 +21,8 @@ export function registerJobsHealthAlertingRule( triggersActionsUi.ruleTypeRegistry.register({ id: ML_ALERT_TYPES.AD_JOBS_HEALTH, description: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.description', { - defaultMessage: 'Alert when anomaly detection jobs experience operational issues.', + defaultMessage: + 'Alert when anomaly detection jobs experience operational issues. Enable suitable alerts for critically important jobs.', }), iconClass: 'bell', documentationUrl(docLinks) { @@ -90,14 +91,15 @@ export function registerJobsHealthAlertingRule( \\{\\{context.message\\}\\} \\{\\{#context.results\\}\\} Job ID: \\{\\{job_id\\}\\} - \\{\\{#datafeed_id\\}\\}Datafeed ID: \\{\\{datafeed_id\\}\\} \\{\\{/datafeed_id\\}\\} - \\{\\{#datafeed_state\\}\\}Datafeed state: \\{\\{datafeed_state\\}\\} \\{\\{/datafeed_state\\}\\} - \\{\\{#memory_status\\}\\}Memory status: \\{\\{memory_status\\}\\} \\{\\{/memory_status\\}\\} - \\{\\{#log_time\\}\\}Memory logging time: \\{\\{log_time\\}\\} \\{\\{/log_time\\}\\} - \\{\\{#failed_category_count\\}\\}Failed category count: \\{\\{failed_category_count\\}\\} \\{\\{/failed_category_count\\}\\} - \\{\\{#annotation\\}\\}Annotation: \\{\\{annotation\\}\\} \\{\\{/annotation\\}\\} - \\{\\{#missed_docs_count\\}\\}Number of missed documents: \\{\\{missed_docs_count\\}\\} \\{\\{/missed_docs_count\\}\\} - \\{\\{#end_timestamp\\}\\}Latest finalized bucket with missing docs: \\{\\{end_timestamp\\}\\} \\{\\{/end_timestamp\\}\\} + \\{\\{#datafeed_id\\}\\}Datafeed ID: \\{\\{datafeed_id\\}\\} + \\{\\{/datafeed_id\\}\\} \\{\\{#datafeed_state\\}\\}Datafeed state: \\{\\{datafeed_state\\}\\} + \\{\\{/datafeed_state\\}\\} \\{\\{#memory_status\\}\\}Memory status: \\{\\{memory_status\\}\\} + \\{\\{/memory_status\\}\\} \\{\\{#log_time\\}\\}Memory logging time: \\{\\{log_time\\}\\} + \\{\\{/log_time\\}\\} \\{\\{#failed_category_count\\}\\}Failed category count: \\{\\{failed_category_count\\}\\} + \\{\\{/failed_category_count\\}\\} \\{\\{#annotation\\}\\}Annotation: \\{\\{annotation\\}\\} + \\{\\{/annotation\\}\\} \\{\\{#missed_docs_count\\}\\}Number of missed documents: \\{\\{missed_docs_count\\}\\} + \\{\\{/missed_docs_count\\}\\} \\{\\{#end_timestamp\\}\\}Latest finalized bucket with missing docs: \\{\\{end_timestamp\\}\\} + \\{\\{/end_timestamp\\}\\} \\{\\{#errors\\}\\}Error message: \\{\\{message\\}\\} \\{\\{/errors\\}\\} \\{\\{/context.results\\}\\} `, } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx index 7080d86498a51..dfaf58eba03d8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx @@ -43,7 +43,6 @@ const baselineStyle: LineAnnotationStyle = { details: { fontFamily: 'Arial', fontSize: 10, - fontStyle: 'bold', fill: euiColorMediumShade, padding: 0, }, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx index a67863ea5f803..1701b7a2bd4fb 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC, useState, useEffect, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, @@ -23,8 +23,9 @@ import { import { deleteJobs } from '../utils'; import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; import { DeleteJobCheckModal } from '../../../../components/delete_job_check_modal'; +import { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; -type ShowFunc = (jobs: Array<{ id: string }>) => void; +type ShowFunc = (jobs: MlSummaryJob[]) => void; interface Props { setShowFunction(showFunc: ShowFunc): void; @@ -49,18 +50,18 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, }; }, []); - function showModal(jobs: any[]) { + const showModal = useCallback((jobs: MlSummaryJob[]) => { setJobIds(jobs.map(({ id }) => id)); setModalVisible(true); setDeleting(false); - } + }, []); - function closeModal() { + const closeModal = useCallback(() => { setModalVisible(false); setCanDelete(false); - } + }, []); - function deleteJob() { + const deleteJob = useCallback(() => { setDeleting(true); deleteJobs(jobIds.map((id) => ({ id }))); @@ -68,7 +69,7 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, closeModal(); refreshJobs(); }, DELETING_JOBS_REFRESH_INTERVAL_MS); - } + }, [jobIds, refreshJobs]); if (modalVisible === false || jobIds.length === 0) { return null; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js index 82adc8df8f344..2bee4cd171637 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js @@ -8,14 +8,24 @@ import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import { getIndexPatternNames } from '../../../../util/index_utils'; +import { JOB_ACTION } from '../../../../../../common/constants/job_actions'; -import { stopDatafeeds, cloneJob, closeJobs, isStartable, isStoppable, isClosable } from '../utils'; +import { + stopDatafeeds, + cloneJob, + closeJobs, + isStartable, + isStoppable, + isClosable, + isResettable, +} from '../utils'; import { getToastNotifications } from '../../../../util/dependency_cache'; import { i18n } from '@kbn/i18n'; export function actionsMenuContent( showEditJobFlyout, showDeleteJobModal, + showResetJobModal, showStartDatafeedModal, refreshJobs, showCreateAlertFlyout @@ -26,6 +36,7 @@ export function actionsMenuContent( const canUpdateDatafeed = checkPermission('canUpdateDatafeed'); const canStartStopDatafeed = checkPermission('canStartStopDatafeed') && mlNodesAvailable(); const canCloseJob = checkPermission('canCloseJob') && mlNodesAvailable(); + const canResetJob = checkPermission('canResetJob') && mlNodesAvailable(); const canCreateMlAlerts = checkPermission('canCreateMlAlerts'); return [ @@ -37,7 +48,7 @@ export function actionsMenuContent( defaultMessage: 'Start datafeed', }), icon: 'play', - enabled: (item) => item.deleting !== true && canStartStopDatafeed, + enabled: (item) => isJobBlocked(item) === false && canStartStopDatafeed, available: (item) => isStartable([item]), onClick: (item) => { showStartDatafeedModal([item]); @@ -53,7 +64,7 @@ export function actionsMenuContent( defaultMessage: 'Stop datafeed', }), icon: 'stop', - enabled: (item) => item.deleting !== true && canStartStopDatafeed, + enabled: (item) => isJobBlocked(item) === false && canStartStopDatafeed, available: (item) => isStoppable([item]), onClick: (item) => { stopDatafeeds([item], refreshJobs); @@ -69,7 +80,7 @@ export function actionsMenuContent( defaultMessage: 'Create alert rule', }), icon: 'bell', - enabled: (item) => item.deleting !== true, + enabled: (item) => isJobBlocked(item) === false, available: () => canCreateMlAlerts, onClick: (item) => { showCreateAlertFlyout([item.id]); @@ -85,7 +96,7 @@ export function actionsMenuContent( defaultMessage: 'Close job', }), icon: 'cross', - enabled: (item) => item.deleting !== true && canCloseJob, + enabled: (item) => isJobBlocked(item) === false && canCloseJob, available: (item) => isClosable([item]), onClick: (item) => { closeJobs([item], refreshJobs); @@ -93,6 +104,22 @@ export function actionsMenuContent( }, 'data-test-subj': 'mlActionButtonCloseJob', }, + { + name: i18n.translate('xpack.ml.jobsList.managementActions.resetJobLabel', { + defaultMessage: 'Reset job', + }), + description: i18n.translate('xpack.ml.jobsList.managementActions.resetJobDescription', { + defaultMessage: 'Reset job', + }), + icon: 'refresh', + enabled: (item) => isResetEnabled(item) && canResetJob, + available: (item) => isResettable([item]), + onClick: (item) => { + showResetJobModal([item]); + closeMenu(true); + }, + 'data-test-subj': 'mlActionButtonResetJob', + }, { name: i18n.translate('xpack.ml.jobsList.managementActions.cloneJobLabel', { defaultMessage: 'Clone job', @@ -106,7 +133,7 @@ export function actionsMenuContent( // the indexPattern the job was created for. An indexPattern could either have been deleted // since the the job was created or the current user doesn't have the required permissions to // access the indexPattern. - return item.deleting !== true && canCreateJob; + return isJobBlocked(item) === false && canCreateJob; }, onClick: (item) => { const indexPatternNames = getIndexPatternNames(); @@ -136,7 +163,7 @@ export function actionsMenuContent( defaultMessage: 'Edit job', }), icon: 'pencil', - enabled: (item) => item.deleting !== true && canUpdateJob && canUpdateDatafeed, + enabled: (item) => isJobBlocked(item) === false && canUpdateJob && canUpdateDatafeed, onClick: (item) => { showEditJobFlyout(item); closeMenu(); @@ -162,6 +189,17 @@ export function actionsMenuContent( ]; } +function isResetEnabled(item) { + if (item.blocked === undefined || item.blocked.reason === JOB_ACTION.RESET) { + return true; + } + return false; +} + +function isJobBlocked(item) { + return item.blocked !== undefined; +} + function closeMenu(now = false) { if (now) { document.querySelector('.euiTable').click(); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js index c09b4afd03443..b1741cc83dc3b 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js @@ -48,7 +48,7 @@ export function ResultLinks({ jobs }) { }, }) : undefined; - const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true; + const jobActionsDisabled = jobs.length === 1 && jobs[0].blocked !== undefined; const { createLinkWithUserDefaults } = useCreateADLinks(); const timeSeriesExplorerLink = useMemo( () => createLinkWithUserDefaults('timeseriesexplorer', jobs), diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index cc6db66dc1cfd..f1258f377f528 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -104,7 +104,7 @@ export class JobsList extends Component { render() { const { loading, isManagementTable, spacesApi } = this.props; const selectionControls = { - selectable: (job) => job.deleting !== true, + selectable: (job) => job.blocked === undefined, selectableMessage: (selectable, rowItem) => selectable === false ? i18n.translate('xpack.ml.jobsList.cannotSelectRowForJobMessage', { @@ -140,7 +140,7 @@ export class JobsList extends Component { render: (item) => ( this.toggleRow(item)} - isDisabled={item.deleting === true} + isDisabled={item.blocked !== undefined} iconType={this.state.itemIdToExpandedRowMap[item.id] ? 'arrowDown' : 'arrowRight'} aria-label={ this.state.itemIdToExpandedRowMap[item.id] @@ -337,6 +337,7 @@ export class JobsList extends Component { actions: actionsMenuContent( this.props.showEditJobFlyout, this.props.showDeleteJobModal, + this.props.showResetJobModal, this.props.showStartDatafeedModal, this.props.refreshJobs, this.props.showCreateAlertFlyout diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 4fdf1c4d3ab11..f0017ada611e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -27,6 +27,7 @@ import { JobDetails } from '../job_details'; import { JobFilterBar } from '../job_filter_bar'; import { EditJobFlyout } from '../edit_job_flyout'; import { DeleteJobModal } from '../delete_job_modal'; +import { ResetJobModal } from '../reset_job_modal'; import { StartDatafeedModal } from '../start_datafeed_modal'; import { MultiJobActions } from '../multi_job_actions'; import { NewJobButton } from '../new_job_button'; @@ -41,7 +42,7 @@ import { RefreshJobsListButton } from '../refresh_jobs_list_button'; import { DELETING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; import { JobListMlAnomalyAlertFlyout } from '../../../../../alerting/ml_alerting_flyout'; -let deletingJobsRefreshTimeout = null; +let blockingJobsRefreshTimeout = null; const filterJobsDebounce = debounce((jobsSummaryList, filterClauses, callback) => { const ss = filterJobs(jobsSummaryList, filterClauses); @@ -62,7 +63,7 @@ export class JobsListView extends Component { selectedJobs: [], itemIdToExpandedRowMap: {}, filterClauses: [], - deletingJobIds: [], + blockingJobIds: [], jobsAwaitingNodeCount: 0, }; @@ -70,6 +71,7 @@ export class JobsListView extends Component { this.showEditJobFlyout = () => {}; this.showDeleteJobModal = () => {}; + this.showResetJobModal = () => {}; this.showStartDatafeedModal = () => {}; this.showCreateAlertFlyout = () => {}; // work around to keep track of whether the component is mounted @@ -105,7 +107,7 @@ export class JobsListView extends Component { componentWillUnmount() { if (this.props.isManagementTable === undefined) { - deletingJobsRefreshTimeout = null; + blockingJobsRefreshTimeout = null; } this._isMounted = false; } @@ -209,6 +211,13 @@ export class JobsListView extends Component { this.showDeleteJobModal = () => {}; }; + setShowResetJobModalFunction = (func) => { + this.showResetJobModal = func; + }; + unsetShowResetJobModalFunction = () => { + this.showResetJobModal = () => {}; + }; + setShowStartDatafeedModalFunction = (func) => { this.showStartDatafeedModal = func; }; @@ -353,17 +362,17 @@ export class JobsListView extends Component { }); jobs.forEach((job) => { - if (job.deleting && this.state.itemIdToExpandedRowMap[job.id]) { + if (job.blocked !== undefined && this.state.itemIdToExpandedRowMap[job.id]) { this.toggleRow(job.id); } }); this.isDoneRefreshing(); - if (jobsSummaryList.some((j) => j.deleting === true)) { + if (jobsSummaryList.some((j) => j.blocked !== undefined)) { // if there are some jobs in a deleting state, start polling for // deleting jobs so we can update the jobs list once the // deleting tasks are over - this.checkDeletingJobTasks(forceRefresh); + this.checkBlockingJobTasks(forceRefresh); } } catch (error) { console.error(error); @@ -372,18 +381,18 @@ export class JobsListView extends Component { } } - async checkDeletingJobTasks(forceRefresh = false) { + async checkBlockingJobTasks(forceRefresh = false) { if (this._isMounted === false) { return; } - const { jobIds: taskJobIds } = await ml.jobs.deletingJobTasks(); - + const { jobs } = await ml.jobs.blockingJobTasks(); + const blockingJobIds = Object.keys(jobs); const taskListHasChanged = - isEqual(taskJobIds.sort(), this.state.deletingJobIds.sort()) === false; + isEqual(blockingJobIds.sort(), this.state.blockingJobIds.sort()) === false; this.setState({ - deletingJobIds: taskJobIds, + blockingJobIds, }); // only reload the jobs list if the contents of the task list has changed @@ -392,10 +401,10 @@ export class JobsListView extends Component { this.refreshJobSummaryList(); } - if (taskJobIds.length > 0 && deletingJobsRefreshTimeout === null) { - deletingJobsRefreshTimeout = setTimeout(() => { - deletingJobsRefreshTimeout = null; - this.checkDeletingJobTasks(); + if (blockingJobIds.length > 0 && blockingJobsRefreshTimeout === null) { + blockingJobsRefreshTimeout = setTimeout(() => { + blockingJobsRefreshTimeout = null; + this.checkBlockingJobTasks(); }, DELETING_JOBS_REFRESH_INTERVAL_MS); } } @@ -515,6 +524,7 @@ export class JobsListView extends Component { allJobIds={jobIds} showStartDatafeedModal={this.showStartDatafeedModal} showDeleteJobModal={this.showDeleteJobModal} + showResetJobModal={this.showResetJobModal} showCreateAlertFlyout={this.showCreateAlertFlyout} refreshJobs={() => this.refreshJobSummaryList(true)} /> @@ -531,6 +541,7 @@ export class JobsListView extends Component { selectJobChange={this.selectJobChange} showEditJobFlyout={this.showEditJobFlyout} showDeleteJobModal={this.showDeleteJobModal} + showResetJobModal={this.showResetJobModal} showStartDatafeedModal={this.showStartDatafeedModal} refreshJobs={() => this.refreshJobSummaryList(true)} jobsViewState={this.props.jobsViewState} @@ -550,6 +561,11 @@ export class JobsListView extends Component { unsetShowFunction={this.unsetShowDeleteJobModalFunction} refreshJobs={() => this.refreshJobSummaryList(true)} /> + this.refreshJobSummaryList(true)} + /> j.deleting); + const anyJobsBlocked = this.props.jobs.some((j) => j.blocked !== undefined); const button = ( @@ -103,6 +111,27 @@ class MultiJobActionsMenuUI extends Component { ); } + if (isResettable(this.props.jobs)) { + items.push( + { + this.props.showResetJobModal(this.props.jobs); + this.closePopover(); + }} + data-test-subj="mlADJobListMultiSelectResetJobActionButton" + > + + + ); + } + if (isStoppable(this.props.jobs)) { items.push( @@ -81,6 +82,7 @@ MultiJobActions.propTypes = { allJobIds: PropTypes.array.isRequired, showStartDatafeedModal: PropTypes.func.isRequired, showDeleteJobModal: PropTypes.func.isRequired, + showResetJobModal: PropTypes.func.isRequired, refreshJobs: PropTypes.func.isRequired, showCreateAlertFlyout: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/index.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/index.ts new file mode 100644 index 0000000000000..71d46a17425e8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { ResetJobModal } from './reset_job_modal'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/open_jobs_warning_callout.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/open_jobs_warning_callout.tsx new file mode 100644 index 0000000000000..39e86cdf1f4ac --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/open_jobs_warning_callout.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; +import { JOB_STATE } from '../../../../../../common/constants/states'; + +interface Props { + jobs: MlSummaryJob[]; +} + +export const OpenJobsWarningCallout: FC = ({ jobs }) => { + const openJobsCount = useMemo(() => jobs.filter((j) => j.jobState !== JOB_STATE.CLOSED).length, [ + jobs, + ]); + + if (openJobsCount === 0) { + return null; + } + + return ( + <> + + } + color="warning" + > + +
+ +
+ + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx new file mode 100644 index 0000000000000..32a7c9b497e5c --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/reset_job_modal/reset_job_modal.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useState, useEffect, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiSpacer, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButtonEmpty, + EuiButton, + EuiText, +} from '@elastic/eui'; + +import { resetJobs } from '../utils'; +import type { MlSummaryJob } from '../../../../../../common/types/anomaly_detection_jobs'; +import { RESETTING_JOBS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants/jobs_list'; +import { OpenJobsWarningCallout } from './open_jobs_warning_callout'; + +type ShowFunc = (jobs: MlSummaryJob[]) => void; + +interface Props { + setShowFunction(showFunc: ShowFunc): void; + unsetShowFunction(): void; + refreshJobs(): void; +} + +export const ResetJobModal: FC = ({ setShowFunction, unsetShowFunction, refreshJobs }) => { + const [resetting, setResetting] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [jobIds, setJobIds] = useState([]); + const [jobs, setJobs] = useState([]); + + useEffect(() => { + if (typeof setShowFunction === 'function') { + setShowFunction(showModal); + } + return () => { + if (typeof unsetShowFunction === 'function') { + unsetShowFunction(); + } + }; + }, []); + + const showModal = useCallback((tempJobs: MlSummaryJob[]) => { + setJobIds(tempJobs.map(({ id }) => id)); + setJobs(tempJobs); + setModalVisible(true); + setResetting(false); + }, []); + + const closeModal = useCallback(() => { + setModalVisible(false); + }, []); + + const resetJob = useCallback(async () => { + setResetting(true); + await resetJobs(jobIds); + closeModal(); + setTimeout(() => { + refreshJobs(); + }, RESETTING_JOBS_REFRESH_INTERVAL_MS); + }, [jobIds, refreshJobs]); + + if (modalVisible === false || jobIds.length === 0) { + return null; + } + + return ( + + + + + + + + <> + + + + + + + <> + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts index 76e1e87312a4a..49df7f3cbb00f 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts @@ -8,4 +8,5 @@ import { CombinedJobWithStats } from '../../../../../common/types/anomaly_detection_jobs'; export function deleteJobs(jobs: Array<{ id: string }>, callback?: () => void): Promise; +export function resetJobs(jobIds: string[], callback?: () => void): Promise; export function loadFullJob(jobId: string): Promise; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index f004fb6bad49d..414d920237e8c 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -17,6 +17,7 @@ import { getToastNotifications } from '../../../util/dependency_cache'; import { ml } from '../../../services/ml_api_service'; import { stringMatch } from '../../../util/string_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; +import { JOB_ACTION } from '../../../../../common/constants/job_actions'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { mlCalendarService } from '../../../services/calendar_service'; import { isPopulatedObject } from '../../../../../common/util/object_utils'; @@ -76,6 +77,12 @@ export function isClosable(jobs) { ); } +export function isResettable(jobs) { + return jobs.some( + (j) => j.jobState === JOB_STATE.CLOSED || j.blocked?.reason === JOB_ACTION.RESET + ); +} + export function forceStartDatafeeds(jobs, start, end, finish = () => {}) { const datafeedIds = jobs.filter((j) => j.hasDatafeed).map((j) => j.datafeedId); mlJobService @@ -165,6 +172,13 @@ function showResults(resp, action) { actionTextPT = i18n.translate('xpack.ml.jobsList.closedActionStatusText', { defaultMessage: 'closed', }); + } else if (action === JOB_ACTION.RESET) { + actionText = i18n.translate('xpack.ml.jobsList.resetActionStatusText', { + defaultMessage: 'reset', + }); + actionTextPT = i18n.translate('xpack.ml.jobsList.resetActionStatusText', { + defaultMessage: 'reset', + }); } const toastNotifications = getToastNotifications(); @@ -283,6 +297,24 @@ export function closeJobs(jobs, finish = () => {}) { }); } +export function resetJobs(jobIds, finish = () => {}) { + mlJobService + .resetJobs(jobIds) + .then((resp) => { + showResults(resp, JOB_ACTION.RESET); + finish(); + }) + .catch((error) => { + getToastNotificationService().displayErrorToast( + error, + i18n.translate('xpack.ml.jobsList.resetJobErrorMessage', { + defaultMessage: 'Jobs failed to reset', + }) + ); + finish(); + }); +} + export function deleteJobs(jobs, finish = () => {}) { const jobIds = jobs.map((j) => j.id); mlJobService diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 8560cdd73153b..4a6f7dbbcc3ff 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -386,6 +386,10 @@ class JobService { return ml.jobs.closeJobs(jIds); } + resetJobs(jIds) { + return ml.jobs.resetJobs(jIds); + } + validateDetector(detector) { return new Promise((resolve, reject) => { if (detector) { diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 81a86e5a7f980..7a75e1a2bdbc0 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { Observable } from 'rxjs'; -import { HttpStart } from 'kibana/public'; +import type { HttpStart } from 'kibana/public'; import { HttpService } from '../http_service'; import { annotations } from './annotations'; @@ -16,16 +17,19 @@ import { resultsApiProvider } from './results'; import { jobsApiProvider } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; import { savedObjectsApiProvider } from './saved_objects'; -import { +import type { MlServerDefaults, MlServerLimits, MlNodeCount, } from '../../../../common/types/ml_server_info'; -import { MlCapabilitiesResponse } from '../../../../common/types/capabilities'; -import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; -import { BucketSpanEstimatorData } from '../../../../common/types/job_service'; -import { +import type { MlCapabilitiesResponse } from '../../../../common/types/capabilities'; +import type { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; +import type { + BucketSpanEstimatorData, + ResetJobsResponse, +} from '../../../../common/types/job_service'; +import type { Job, JobStats, Datafeed, @@ -35,8 +39,8 @@ import { ModelSnapshot, IndicesOptions, } from '../../../../common/types/anomaly_detection_jobs'; -import { FieldHistogramRequestConfig } from '../../datavisualizer/index_based/common/request'; -import { DataRecognizerConfigResponse, Module } from '../../../../common/types/modules'; +import type { FieldHistogramRequestConfig } from '../../datavisualizer/index_based/common/request'; +import type { DataRecognizerConfigResponse, Module } from '../../../../common/types/modules'; import { getHttp } from '../../util/dependency_cache'; import type { RuntimeMappings } from '../../../../common/types/fields'; @@ -151,14 +155,14 @@ export function mlApiServicesProvider(httpService: HttpService) { }, deleteJob({ jobId }: { jobId: string }) { - return httpService.http({ + return httpService.http({ path: `${basePath()}/anomaly_detectors/${jobId}`, method: 'DELETE', }); }, forceDeleteJob({ jobId }: { jobId: string }) { - return httpService.http({ + return httpService.http({ path: `${basePath()}/anomaly_detectors/${jobId}?force=true`, method: 'DELETE', }); @@ -173,6 +177,13 @@ export function mlApiServicesProvider(httpService: HttpService) { }); }, + resetJob({ jobId }: { jobId: string }) { + return httpService.http({ + path: `${basePath()}/anomaly_detectors/${jobId}/_reset`, + method: 'POST', + }); + }, + estimateBucketSpan(obj: BucketSpanEstimatorData) { const body = JSON.stringify(obj); return httpService.http({ diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index bf0eee81158c0..96c5e1abce170 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -18,6 +18,7 @@ import type { IndicesOptions, } from '../../../../common/types/anomaly_detection_jobs'; import type { JobMessage } from '../../../../common/types/audit_message'; +import type { JobAction } from '../../../../common/constants/job_actions'; import type { AggFieldNamePair, RuntimeMappings } from '../../../../common/types/fields'; import type { ExistingJobsAndGroups } from '../job_service'; import type { @@ -27,7 +28,11 @@ import type { } from '../../../../common/types/categories'; import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job'; import type { Category } from '../../../../common/types/categories'; -import type { JobsExistResponse, BulkCreateResults } from '../../../../common/types/job_service'; +import type { + JobsExistResponse, + BulkCreateResults, + ResetJobsResponse, +} from '../../../../common/types/job_service'; import { ML_BASE_PATH } from '../../../../common/constants/app'; export const jobsApiProvider = (httpService: HttpService) => ({ @@ -127,6 +132,15 @@ export const jobsApiProvider = (httpService: HttpService) => ({ }); }, + resetJobs(jobIds: string[]) { + const body = JSON.stringify({ jobIds }); + return httpService.http({ + path: `${ML_BASE_PATH}/jobs/reset_jobs`, + method: 'POST', + body, + }); + }, + forceStopAndCloseJob(jobId: string) { const body = JSON.stringify({ jobId }); return httpService.http<{ success: boolean }>({ @@ -169,9 +183,9 @@ export const jobsApiProvider = (httpService: HttpService) => ({ }); }, - deletingJobTasks() { - return httpService.http({ - path: `${ML_BASE_PATH}/jobs/deleting_jobs_tasks`, + blockingJobTasks() { + return httpService.http>({ + path: `${ML_BASE_PATH}/jobs/blocking_jobs_tasks`, method: 'GET', }); }, diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts index b345cf8c1245c..ffaa26fc949ee 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts @@ -11,9 +11,39 @@ import type { Logger } from 'kibana/server'; import { MlClient } from '../ml_client'; import { MlJob, MlJobStats } from '@elastic/elasticsearch/api/types'; import { AnnotationService } from '../../models/annotation_service/annotation'; +import { JobsHealthExecutorOptions } from './register_jobs_monitoring_rule_type'; +import { JobAuditMessagesService } from '../../models/job_audit_messages/job_audit_messages'; +import { DeepPartial } from '../../../common/types/common'; const MOCK_DATE_NOW = 1487076708000; +function getDefaultExecutorOptions( + overrides: DeepPartial = {} +): JobsHealthExecutorOptions { + return ({ + state: {}, + startedAt: new Date('2021-08-12T13:13:39.396Z'), + previousStartedAt: new Date('2021-08-12T13:13:27.396Z'), + spaceId: 'default', + namespace: undefined, + name: 'ml-health-check', + tags: [], + createdBy: 'elastic', + updatedBy: 'elastic', + rule: { + name: 'ml-health-check', + tags: [], + consumer: 'alerts', + producer: 'ml', + ruleTypeId: 'xpack.ml.anomaly_detection_jobs_health', + ruleTypeName: 'Anomaly detection jobs health', + enabled: true, + schedule: { interval: '10s' }, + }, + ...overrides, + } as unknown) as JobsHealthExecutorOptions; +} + describe('JobsHealthService', () => { const mlClient = ({ getJobs: jest.fn().mockImplementation(({ job_id: jobIds = [] }) => { @@ -117,6 +147,12 @@ describe('JobsHealthService', () => { }), } as unknown) as jest.Mocked; + const jobAuditMessagesService = ({ + getJobsErrors: jest.fn().mockImplementation((jobIds: string) => { + return Promise.resolve({}); + }), + } as unknown) as jest.Mocked; + const logger = ({ warn: jest.fn(), info: jest.fn(), @@ -127,6 +163,7 @@ describe('JobsHealthService', () => { mlClient, datafeedsService, annotationService, + jobAuditMessagesService, logger ); @@ -143,42 +180,52 @@ describe('JobsHealthService', () => { test('returns empty results when no jobs provided', async () => { // act - const executionResult = await jobHealthService.getTestsResults('testRule', { - testsConfig: null, - includeJobs: { - jobIds: ['*'], - groupIds: [], - }, - excludeJobs: null, - }); + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule' }, + params: { + testsConfig: null, + includeJobs: { + jobIds: ['*'], + groupIds: [], + }, + excludeJobs: null, + }, + }) + ); expect(logger.warn).toHaveBeenCalledWith('Rule "testRule" does not have associated jobs.'); expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); expect(executionResult).toEqual([]); }); test('returns empty results and does not perform datafeed check when test is disabled', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule', { - testsConfig: { - datafeed: { - enabled: false, - }, - behindRealtime: null, - delayedData: { - enabled: false, - docsCount: null, - timeInterval: null, - }, - errorMessages: null, - mml: { - enabled: false, + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule' }, + params: { + testsConfig: { + datafeed: { + enabled: false, + }, + behindRealtime: null, + delayedData: { + enabled: false, + docsCount: null, + timeInterval: null, + }, + errorMessages: null, + mml: { + enabled: false, + }, + }, + includeJobs: { + jobIds: ['test_job_01'], + groupIds: [], + }, + excludeJobs: null, }, - }, - includeJobs: { - jobIds: ['test_job_01'], - groupIds: [], - }, - excludeJobs: null, - }); + }) + ); expect(logger.warn).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith(`Performing health checks for job IDs: test_job_01`); expect(datafeedsService.getDatafeedByJobId).not.toHaveBeenCalled(); @@ -186,27 +233,32 @@ describe('JobsHealthService', () => { }); test('takes into account delayed data params', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule_04', { - testsConfig: { - delayedData: { - enabled: true, - docsCount: 10, - timeInterval: '4h', + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule_04' }, + params: { + testsConfig: { + delayedData: { + enabled: true, + docsCount: 10, + timeInterval: '4h', + }, + behindRealtime: { enabled: false, timeInterval: null }, + mml: { enabled: false }, + datafeed: { enabled: false }, + errorMessages: { enabled: false }, + }, + includeJobs: { + jobIds: [], + groupIds: ['test_group'], + }, + excludeJobs: { + jobIds: ['test_job_03'], + groupIds: [], + }, }, - behindRealtime: { enabled: false, timeInterval: null }, - mml: { enabled: false }, - datafeed: { enabled: false }, - errorMessages: { enabled: false }, - }, - includeJobs: { - jobIds: [], - groupIds: ['test_group'], - }, - excludeJobs: { - jobIds: ['test_job_03'], - groupIds: [], - }, - }); + }) + ); expect(annotationService.getDelayedDataAnnotations).toHaveBeenCalledWith({ jobIds: ['test_job_01', 'test_job_02'], @@ -234,17 +286,22 @@ describe('JobsHealthService', () => { }); test('returns results based on provided selection', async () => { - const executionResult = await jobHealthService.getTestsResults('testRule_03', { - testsConfig: null, - includeJobs: { - jobIds: [], - groupIds: ['test_group'], - }, - excludeJobs: { - jobIds: ['test_job_03'], - groupIds: [], - }, - }); + const executionResult = await jobHealthService.getTestsResults( + getDefaultExecutorOptions({ + rule: { name: 'testRule_03' }, + params: { + testsConfig: null, + includeJobs: { + jobIds: [], + groupIds: ['test_group'], + }, + excludeJobs: { + jobIds: ['test_job_03'], + groupIds: [], + }, + }, + }) + ); expect(logger.warn).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith( `Performing health checks for job IDs: test_job_01, test_job_02` diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts index 52e17fed7a414..bcae57e558573 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts @@ -11,10 +11,7 @@ import { i18n } from '@kbn/i18n'; import { Logger } from 'kibana/server'; import { MlJob } from '@elastic/elasticsearch/api/types'; import { MlClient } from '../ml_client'; -import { - AnomalyDetectionJobsHealthRuleParams, - JobSelection, -} from '../../routes/schemas/alerting_schema'; +import { JobSelection } from '../../routes/schemas/alerting_schema'; import { datafeedsProvider, DatafeedsService } from '../../models/job_service/datafeeds'; import { ALL_JOBS_SELECTION, HEALTH_CHECK_NAMES } from '../../../common/constants/alerts'; import { DatafeedStats } from '../../../common/types/anomaly_detection_jobs'; @@ -22,6 +19,7 @@ import { GetGuards } from '../../shared_services/shared_services'; import { AnomalyDetectionJobsHealthAlertContext, DelayedDataResponse, + JobsHealthExecutorOptions, MmlTestResponse, NotStartedDatafeedResponse, } from './register_jobs_monitoring_rule_type'; @@ -33,6 +31,10 @@ import { AnnotationService } from '../../models/annotation_service/annotation'; import { annotationServiceProvider } from '../../models/annotation_service'; import { parseInterval } from '../../../common/util/parse_interval'; import { isDefined } from '../../../common/types/guards'; +import { + jobAuditMessagesProvider, + JobAuditMessagesService, +} from '../../models/job_audit_messages/job_audit_messages'; interface TestResult { name: string; @@ -45,6 +47,7 @@ export function jobsHealthServiceProvider( mlClient: MlClient, datafeedsService: DatafeedsService, annotationService: AnnotationService, + jobAuditMessagesService: JobAuditMessagesService, logger: Logger ) { /** @@ -236,13 +239,25 @@ export function jobsHealthServiceProvider( return annotations; }, + /** + * Retrieves a list of the latest errors per jobs. + * @param jobIds List of job IDs. + * @param previousStartedAt Time of the previous rule execution. As we intend to notify + * about an error only once, limit the scope of the errors search. + */ + async getErrorsReport(jobIds: string[], previousStartedAt: Date) { + return await jobAuditMessagesService.getJobsErrors(jobIds, previousStartedAt.getTime()); + }, /** * Retrieves report grouped by test. */ - async getTestsResults( - ruleInstanceName: string, - { testsConfig, includeJobs, excludeJobs }: AnomalyDetectionJobsHealthRuleParams - ): Promise { + async getTestsResults(executorOptions: JobsHealthExecutorOptions): Promise { + const { + rule, + previousStartedAt, + params: { testsConfig, includeJobs, excludeJobs }, + } = executorOptions; + const config = getResultJobsHealthRuleConfig(testsConfig); const results: TestsResults = []; @@ -251,7 +266,7 @@ export function jobsHealthServiceProvider( const jobIds = getJobIds(jobs); if (jobIds.length === 0) { - logger.warn(`Rule "${ruleInstanceName}" does not have associated jobs.`); + logger.warn(`Rule "${rule.name}" does not have associated jobs.`); return results; } @@ -334,6 +349,26 @@ export function jobsHealthServiceProvider( } } + if (config.errorMessages.enabled && previousStartedAt) { + const response = await this.getErrorsReport(jobIds, previousStartedAt); + if (response.length > 0) { + results.push({ + name: HEALTH_CHECK_NAMES.errorMessages.name, + context: { + results: response, + message: i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesMessage', + { + defaultMessage: + '{jobsCount, plural, one {# job contains} other {# jobs contain}} errors in the messages.', + values: { jobsCount: response.length }, + } + ), + }, + }); + } + } + return results; }, }; @@ -360,6 +395,7 @@ export function getJobsHealthServiceProvider(getGuards: GetGuards) { mlClient, datafeedsProvider(scopedClient, mlClient), annotationServiceProvider(scopedClient), + jobAuditMessagesProvider(scopedClient, mlClient), logger ).getTestsResults(...args) ); diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index 063d8ad5a8980..c49c169d3bd21 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -22,6 +22,8 @@ import { AlertInstanceState, AlertTypeState, } from '../../../../alerting/common'; +import { JobsErrorsResponse } from '../../models/job_audit_messages/job_audit_messages'; +import { AlertExecutorOptions } from '../../../../alerting/server'; type ModelSizeStats = MlJobStats['model_size_stats']; @@ -55,7 +57,8 @@ export interface DelayedDataResponse { export type AnomalyDetectionJobHealthResult = | MmlTestResponse | NotStartedDatafeedResponse - | DelayedDataResponse; + | DelayedDataResponse + | JobsErrorsResponse[number]; export type AnomalyDetectionJobsHealthAlertContext = { results: AnomalyDetectionJobHealthResult[]; @@ -69,10 +72,18 @@ export type AnomalyDetectionJobRealtimeIssue = typeof ANOMALY_DETECTION_JOB_REAL export const REALTIME_ISSUE_DETECTED: ActionGroup = { id: ANOMALY_DETECTION_JOB_REALTIME_ISSUE, name: i18n.translate('xpack.ml.jobsHealthAlertingRule.actionGroupName', { - defaultMessage: 'Real-time issue detected', + defaultMessage: 'Issue detected', }), }; +export type JobsHealthExecutorOptions = AlertExecutorOptions< + AnomalyDetectionJobsHealthRuleParams, + Record, + Record, + AnomalyDetectionJobsHealthAlertContext, + AnomalyDetectionJobRealtimeIssue +>; + export function registerJobsMonitoringRuleType({ alerting, mlServicesProviders, @@ -120,14 +131,16 @@ export function registerJobsMonitoringRuleType({ producer: PLUGIN_ID, minimumLicenseRequired: MINIMUM_FULL_LICENSE, isExportable: true, - async executor({ services, params, alertId, state, previousStartedAt, startedAt, name, rule }) { + async executor(options) { + const { services, name } = options; + const fakeRequest = {} as KibanaRequest; const { getTestsResults } = mlServicesProviders.jobsHealthServiceProvider( services.savedObjectsClient, fakeRequest, logger ); - const executionResult = await getTestsResults(name, params); + const executionResult = await getTestsResults(options); if (executionResult.length > 0) { logger.info( diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index 93c2124eae8d1..452bb803997d7 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -51,7 +51,7 @@ describe('check_capabilities', () => { ); const { capabilities } = await getCapabilities(); const count = Object.keys(capabilities).length; - expect(count).toBe(30); + expect(count).toBe(31); }); }); @@ -88,6 +88,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); @@ -137,6 +138,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteJob).toBe(true); expect(capabilities.canOpenJob).toBe(true); expect(capabilities.canCloseJob).toBe(true); + expect(capabilities.canResetJob).toBe(true); expect(capabilities.canForecastJob).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(true); expect(capabilities.canUpdateJob).toBe(true); @@ -185,6 +187,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); @@ -233,6 +236,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); @@ -281,6 +285,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); @@ -331,6 +336,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts index 0a9c76893dd0b..3829c975d057d 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts @@ -474,6 +474,10 @@ export function getMlClient( await jobIdsCheck('anomaly-detector', p); return mlClient.updateJob(...p); }, + async resetJob(...p: Parameters) { + await jobIdsCheck('anomaly-detector', p); + return mlClient.resetJob(...p); + }, async updateModelSnapshot(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); return mlClient.updateModelSnapshot(...p); diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts index 98ed76319a0f7..fcda1a2a3ea73 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts @@ -54,6 +54,10 @@ export function isClearable(index?: string): boolean { return false; } +export type JobsErrorsResponse = Array<{ job_id: string; errors: JobMessage[] }>; + +export type JobAuditMessagesService = ReturnType; + export function jobAuditMessagesProvider( { asInternalUser }: IScopedClusterClient, mlClient: MlClient @@ -178,7 +182,10 @@ export function jobAuditMessagesProvider( return { messages, notificationIndices }; } - // search highest, most recent audit messages for all jobs for the last 24hrs. + /** + * Search highest, most recent audit messages for all jobs for the last 24hrs. + * @param jobIds + */ async function getAuditMessagesSummary(jobIds: string[]): Promise { // TODO This is the current default value of the cluster setting `search.max_buckets`. // This should possibly consider the real settings in a future update. @@ -400,9 +407,70 @@ export function jobAuditMessagesProvider( return (Object.keys(LEVEL) as LevelName[])[Object.values(LEVEL).indexOf(level)]; } + /** + * Retrieve list of errors per job. + * @param jobIds + */ + async function getJobsErrors(jobIds: string[], earliestMs?: number): Promise { + const { body } = await asInternalUser.search({ + index: ML_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + size: 0, + body: { + query: { + bool: { + filter: [ + ...(earliestMs ? [{ range: { timestamp: { gte: earliestMs } } }] : []), + { terms: { job_id: jobIds } }, + { + term: { level: { value: MESSAGE_LEVEL.ERROR } }, + }, + ], + }, + }, + aggs: { + by_job: { + terms: { + field: 'job_id', + size: jobIds.length, + }, + aggs: { + latest_errors: { + top_hits: { + size: 10, + sort: [ + { + timestamp: { + order: 'desc', + }, + }, + ], + }, + }, + }, + }, + }, + }, + }); + + const errors = body.aggregations!.by_job as estypes.AggregationsTermsAggregate<{ + key: string; + doc_count: number; + latest_errors: Pick, 'hits'>; + }>; + + return errors.buckets.map((bucket) => { + return { + job_id: bucket.key, + errors: bucket.latest_errors.hits.hits.map((v) => v._source!), + }; + }); + } + return { getJobAuditMessages, getAuditMessagesSummary, clearJobAuditMessages, + getJobsErrors, }; } diff --git a/x-pack/plugins/ml/server/models/job_service/error_utils.ts b/x-pack/plugins/ml/server/models/job_service/error_utils.ts index 81a86b1ee5ca0..a1d6c3fcc35d4 100644 --- a/x-pack/plugins/ml/server/models/job_service/error_utils.ts +++ b/x-pack/plugins/ml/server/models/job_service/error_utils.ts @@ -7,9 +7,10 @@ import { i18n } from '@kbn/i18n'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; +import { JobAction } from '../../../common/constants/job_actions'; const REQUEST_TIMEOUT_NAME = 'RequestTimeout'; -type ACTION_STATE = DATAFEED_STATE | JOB_STATE; +type ACTION_STATE = DATAFEED_STATE | JOB_STATE | JobAction; export function isRequestTimeout(error: { name: string }) { return error.name === REQUEST_TIMEOUT_NAME; diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index ee336c96a9c0d..4922608487f66 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; @@ -14,6 +13,13 @@ import { parseTimeIntervalForJob, } from '../../../common/util/job_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; +import { + getJobActionString, + JOB_ACTION_TASK, + JOB_ACTION_TASKS, + JOB_ACTION, + JobAction, +} from '../../../common/constants/job_actions'; import { MlSummaryJob, AuditMessage, @@ -27,6 +33,7 @@ import { MlJobsStatsResponse, JobsExistResponse, BulkCreateResults, + ResetJobsResponse, } from '../../../common/types/job_service'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds'; @@ -145,6 +152,29 @@ export function jobsProvider( return results; } + async function resetJobs(jobIds: string[]) { + const results: ResetJobsResponse = {}; + for (const jobId of jobIds) { + try { + const { + // @ts-expect-error @elastic-elasticsearch resetJob response incorrect, missing task + body: { task }, + } = await mlClient.resetJob({ + job_id: jobId, + wait_for_completion: false, + }); + results[jobId] = { reset: true, task }; + } catch (error) { + if (isRequestTimeout(error)) { + return fillResultsWithTimeouts(results, jobId, jobIds, JOB_ACTION.RESET); + } else { + results[jobId] = { reset: false, error: error.body }; + } + } + } + return results; + } + async function forceStopAndCloseJob(jobId: string) { const datafeedIds = await getDatafeedIdsByJobId(); const datafeedId = datafeedIds[jobId]; @@ -181,10 +211,6 @@ export function jobsProvider( // fail silently } - const deletingStr = i18n.translate('xpack.ml.models.jobService.deletingJob', { - defaultMessage: 'deleting', - }); - const jobs = fullJobsList.map((job) => { const hasDatafeed = isPopulatedObject(job.datafeed_config); const dataCounts = job.data_counts; @@ -201,7 +227,7 @@ export function jobsProvider( parseTimeIntervalForJob(job.analysis_config?.bucket_span) ), memory_status: job.model_size_stats ? job.model_size_stats.memory_status : '', - jobState: job.deleting === true ? deletingStr : job.state, + jobState: job.blocked === undefined ? job.state : getJobActionString(job.blocked.reason), hasDatafeed, datafeedId: hasDatafeed && job.datafeed_config.datafeed_id ? job.datafeed_config.datafeed_id : '', @@ -217,11 +243,12 @@ export function jobsProvider( isSingleMetricViewerJob: errorMessage === undefined, isNotSingleMetricViewerJobMessage: errorMessage, nodeName: job.node ? job.node.name : undefined, - deleting: job.deleting || undefined, + blocked: job.blocked ?? undefined, awaitingNodeAssignment: isJobAwaitingNodeAssignment(job), alertingRules: job.alerting_rules, jobTags: job.custom_settings?.job_tags ?? {}, }; + if (jobIds.find((j) => j === tempJob.id)) { tempJob.fullJob = job; } @@ -459,21 +486,25 @@ export function jobsProvider( return jobs; } - async function deletingJobTasks() { - const actions = ['cluster:admin/xpack/ml/job/delete']; - const detailed = true; - const jobIds: string[] = []; + async function blockingJobTasks() { + const jobs: Array> = []; try { const { body } = await asInternalUser.tasks.list({ - actions, - detailed, + actions: JOB_ACTION_TASKS, + detailed: true, }); - if (body.nodes) { - Object.keys(body.nodes).forEach((nodeId) => { - const tasks = body.nodes![nodeId].tasks; - Object.keys(tasks).forEach((taskId) => { - jobIds.push(tasks[taskId].description!.replace(/^delete-job-/, '')); + if (body.nodes !== undefined) { + Object.values(body.nodes).forEach(({ tasks }) => { + Object.values(tasks).forEach(({ action, description }) => { + if (description === undefined) { + return; + } + if (JOB_ACTION_TASK[action] === JOB_ACTION.DELETE) { + jobs.push({ [description.replace(/^delete-job-/, '')]: JOB_ACTION.DELETE }); + } else { + jobs.push({ [description]: JOB_ACTION_TASK[action] }); + } }); }); } @@ -481,12 +512,16 @@ export function jobsProvider( // if the user doesn't have permission to load the task list, // use the jobs list to get the ids of deleting jobs const { - body: { jobs }, - } = await mlClient.getJobs(); + body: { jobs: tempJobs }, + } = await mlClient.getJobs(); - jobIds.push(...jobs.filter((j) => j.deleting === true).map((j) => j.job_id)); + jobs.push( + ...tempJobs + .filter((j) => j.blocked !== undefined) + .map((j) => ({ [j.job_id]: j.blocked!.reason })) + ); } - return { jobIds }; + return { jobs }; } // Checks if each of the jobs in the specified list of IDs exist. @@ -613,12 +648,13 @@ export function jobsProvider( forceDeleteJob, deleteJobs, closeJobs, + resetJobs, forceStopAndCloseJob, jobsSummary, jobsWithTimerange, getJobForCloning, createFullJobsList, - deletingJobTasks, + blockingJobTasks, jobsExist, getAllJobAndGroupIds, getLookBackProgress, diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index c5be5e1c9ef2d..1403ce2a7b4db 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -22,6 +22,8 @@ import { getModelSnapshotsSchema, updateModelSnapshotsSchema, updateModelSnapshotBodySchema, + forceQuerySchema, + jobResetQuerySchema, } from './schemas/anomaly_detectors_schema'; /** @@ -270,13 +272,14 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { * @apiDescription Closes an anomaly detection job. * * @apiSchema (params) jobIdSchema + * @apiSchema (query) forceQuerySchema */ router.post( { path: '/api/ml/anomaly_detectors/{jobId}/_close', validate: { params: jobIdSchema, - query: schema.object({ force: schema.maybe(schema.boolean()) }), + query: forceQuerySchema, }, options: { tags: ['access:ml:canCloseJob'], @@ -301,6 +304,46 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { }) ); + /** + * @apiGroup AnomalyDetectors + * + * @api {post} /api/ml/anomaly_detectors/:jobId/_reset Reset specified job + * @apiName ResetAnomalyDetectorsJob + * @apiDescription Resets an anomaly detection job. + * + * @apiSchema (params) jobIdSchema + * @apiSchema (query) jobResetQuerySchema + */ + router.post( + { + path: '/api/ml/anomaly_detectors/{jobId}/_reset', + validate: { + params: jobIdSchema, + query: jobResetQuerySchema, + }, + options: { + tags: ['access:ml:canCloseJob'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { + try { + const options: { job_id: string; wait_for_completion?: boolean } = { + // TODO change this to correct resetJob request type + job_id: request.params.jobId, + ...(request.query.wait_for_completion !== undefined + ? { wait_for_completion: request.query.wait_for_completion } + : {}), + }; + const { body } = await mlClient.resetJob(options); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup AnomalyDetectors * @@ -309,13 +352,14 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { * @apiDescription Deletes specified anomaly detection job. * * @apiSchema (params) jobIdSchema + * @apiSchema (query) forceQuerySchema */ router.delete( { path: '/api/ml/anomaly_detectors/{jobId}', validate: { params: jobIdSchema, - query: schema.object({ force: schema.maybe(schema.boolean()) }), + query: forceQuerySchema, }, options: { tags: ['access:ml:canDeleteJob'], diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 646a250b96686..b0e94c60f3cb5 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -88,7 +88,7 @@ "TopCategories", "DatafeedPreview", "UpdateGroups", - "DeletingJobTasks", + "BlockingJobTasks", "DeleteJobs", "RevertModelSnapshot", "BulkCreateJobs", diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 63310827ad989..81ef1818383b1 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -174,6 +174,40 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { }) ); + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/reset_jobs Reset multiple jobs + * @apiName ResetJobs + * @apiDescription Resets one or more anomaly detection jobs + * + * @apiSchema (body) jobIdsSchema + */ + router.post( + { + path: '/api/ml/jobs/reset_jobs', + validate: { + body: jobIdsSchema, + }, + options: { + tags: ['access:ml:canResetJob'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { + try { + const { resetJobs } = jobServiceProvider(client, mlClient); + const { jobIds } = request.body; + const resp = await resetJobs(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup JobService * @@ -422,13 +456,13 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { /** * @apiGroup JobService * - * @api {get} /api/ml/jobs/deleting_jobs_tasks Get deleting job tasks - * @apiName DeletingJobTasks - * @apiDescription Gets the ids of deleting anomaly detection jobs + * @api {get} /api/ml/jobs/blocking_jobs_tasks Get blocking job tasks + * @apiName BlockingJobTasks + * @apiDescription Gets the ids of deleting, resetting or reverting anomaly detection jobs */ router.get( { - path: '/api/ml/jobs/deleting_jobs_tasks', + path: '/api/ml/jobs/blocking_jobs_tasks', validate: false, options: { tags: ['access:ml:canGetJobs'], @@ -436,8 +470,8 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { }, routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, response }) => { try { - const { deletingJobTasks } = jobServiceProvider(client, mlClient); - const resp = await deletingJobTasks(); + const { blockingJobTasks } = jobServiceProvider(client, mlClient); + const resp = await blockingJobTasks(); return response.ok({ body: resp, diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 158e522ddf9b3..2b93a3a84457d 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -219,3 +219,13 @@ export const updateModelSnapshotBodySchema = schema.object({ }); export const forecastAnomalyDetector = schema.object({ duration: schema.any() }); + +export const jobResetQuerySchema = schema.object({ + /** wait for completion */ + wait_for_completion: schema.maybe(schema.boolean()), +}); + +export const forceQuerySchema = schema.object({ + /** force close */ + force: schema.maybe(schema.boolean()), +}); diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 7d82006c6b999..0e02812af28fb 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -2,6 +2,10 @@ "id": "monitoring", "version": "8.0.0", "kibanaVersion": "kibana", + "owner": { + "owner": "Stack Monitoring", + "githubTeam": "stack-monitoring-ui" + }, "configPath": ["monitoring"], "requiredPlugins": [ "licensing", diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts index b180c15b4487a..e8ba66c332778 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts @@ -4,9 +4,52 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { useEffect, useState } from 'react'; +import { isEmpty } from 'lodash'; +import { usePluginContext } from '../../../../hooks/use_plugin_context'; +import { parseAlert } from '../../../../pages/alerts/parse_alert'; +import { TopAlert } from '../../../../pages/alerts/'; +import { useKibana } from '../../../../utils/kibana_react'; import { Ecs } from '../../../../../../cases/common'; // no alerts in observability so far // dummy hook for now as hooks cannot be called conditionally export const useFetchAlertData = (): [boolean, Record] => [false, {}]; + +export const useFetchAlertDetail = (alertId: string): [boolean, TopAlert | null] => { + const { http } = useKibana().services; + const [loading, setLoading] = useState(false); + const { observabilityRuleTypeRegistry } = usePluginContext(); + const [alert, setAlert] = useState(null); + + useEffect(() => { + const abortCtrl = new AbortController(); + const fetchData = async () => { + try { + setLoading(true); + const response = await http.get('/internal/rac/alerts', { + query: { + id: alertId, + }, + }); + if (response) { + const parsedAlert = parseAlert(observabilityRuleTypeRegistry)(response); + setAlert(parsedAlert); + setLoading(false); + } + } catch (error) { + setAlert(null); + } + }; + + if (!isEmpty(alertId) && loading === false && alert === null) { + fetchData(); + } + return () => { + abortCtrl.abort(); + }; + }, [http, alertId, alert, loading, observabilityRuleTypeRegistry]); + + return [loading, alert]; +}; diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index 9d18381573e32..52a840a6e5447 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, Suspense } from 'react'; import { casesBreadcrumbs, getCaseDetailsUrl, @@ -15,10 +15,12 @@ import { useFormatUrl, } from '../../../../pages/cases/links'; import { Case } from '../../../../../../cases/common'; -import { useFetchAlertData } from './helpers'; +import { useFetchAlertData, useFetchAlertDetail } from './helpers'; import { useKibana } from '../../../../utils/kibana_react'; +import { usePluginContext } from '../../../../hooks/use_plugin_context'; import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; import { observabilityAppId } from '../../../../../common'; +import { LazyAlertsFlyout } from '../../../..'; interface Props { caseId: string; @@ -41,14 +43,17 @@ export interface CaseProps extends Props { export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { const [caseTitle, setCaseTitle] = useState(null); + const { observabilityRuleTypeRegistry } = usePluginContext(); const { cases: casesUi, - application: { getUrlForApp, navigateToUrl }, + application: { getUrlForApp, navigateToUrl, navigateToApp }, } = useKibana().services; const allCasesLink = getCaseUrl(); const { formatUrl } = useFormatUrl(); const href = formatUrl(allCasesLink); + const [selectedAlertId, setSelectedAlertId] = useState(''); + useBreadcrumbs([ { ...casesBreadcrumbs.cases, href }, ...(caseTitle !== null @@ -80,41 +85,78 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = }), [caseId, formatUrl, subCaseId] ); - const casesUrl = `${getUrlForApp(observabilityAppId)}/cases`; - return casesUi.getCaseView({ - allCasesNavigation: { - href: allCasesHref, - onClick: async (ev) => { - if (ev != null) { - ev.preventDefault(); - } - return navigateToUrl(casesUrl); - }, - }, - caseDetailsNavigation: { - href: caseDetailsHref, - onClick: async (ev) => { - if (ev != null) { - ev.preventDefault(); - } - return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`); - }, - }, - caseId, - configureCasesNavigation: { - href: configureCasesHref, - onClick: async (ev) => { - if (ev != null) { - ev.preventDefault(); - } - return navigateToUrl(`${casesUrl}${configureCasesLink}`); - }, - }, - getCaseDetailHrefWithCommentId, - onCaseDataSuccess, - subCaseId, - useFetchAlertData, - userCanCrud, - }); + + const handleFlyoutClose = useCallback(() => { + setSelectedAlertId(''); + }, []); + + const [alertLoading, alert] = useFetchAlertDetail(selectedAlertId); + + return ( + <> + {alertLoading === false && alert && selectedAlertId !== '' && ( + + + + )} + {casesUi.getCaseView({ + allCasesNavigation: { + href: allCasesHref, + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } + return navigateToUrl(casesUrl); + }, + }, + caseDetailsNavigation: { + href: caseDetailsHref, + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } + return navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id: caseId })}`); + }, + }, + caseId, + configureCasesNavigation: { + href: configureCasesHref, + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } + return navigateToUrl(`${casesUrl}${configureCasesLink}`); + }, + }, + ruleDetailsNavigation: { + href: (ruleId) => { + return getUrlForApp('management', { + path: `/insightsAndAlerting/triggersActions/rule/${ruleId}`, + }); + }, + onClick: async (ruleId, ev) => { + if (ev != null) { + ev.preventDefault(); + } + return navigateToApp('management', { + path: `/insightsAndAlerting/triggersActions/rule/${ruleId}`, + }); + }, + }, + getCaseDetailHrefWithCommentId, + onCaseDataSuccess, + subCaseId, + useFetchAlertData, + showAlertDetails: (alertId) => { + setSelectedAlertId(alertId); + }, + userCanCrud, + })} + + ); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 8cd8977fcf741..62d828b337c2d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -52,7 +52,7 @@ describe('ExploratoryViewHeader', function () { to: 'now', }, }, - true + { openInNewTab: true } ); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index ded56ec9e817f..bfa457ee4025f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -69,7 +69,9 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { timeRange, attributes: lensAttributes, }, - true + { + openInNewTab: true, + } ); } }} diff --git a/x-pack/plugins/observability/public/hooks/use_alert_permission.ts b/x-pack/plugins/observability/public/hooks/use_alert_permission.ts new file mode 100644 index 0000000000000..509324e00f650 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_alert_permission.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; +import { RecursiveReadonly } from '@kbn/utility-types'; + +export interface UseGetUserAlertsPermissionsProps { + crud: boolean; + read: boolean; + loading: boolean; + featureId: string | null; +} + +export const useGetUserAlertsPermissions = ( + uiCapabilities: RecursiveReadonly>, + featureId?: string +): UseGetUserAlertsPermissionsProps => { + const [alertsPermissions, setAlertsPermissions] = useState({ + crud: false, + read: false, + loading: true, + featureId: null, + }); + + useEffect(() => { + if (!featureId || !uiCapabilities[featureId]) { + setAlertsPermissions({ + crud: false, + read: false, + loading: false, + featureId: null, + }); + } else { + setAlertsPermissions((currentAlertPermissions) => { + if (currentAlertPermissions.featureId === featureId) { + return currentAlertPermissions; + } + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities[featureId].save === 'boolean' + ? uiCapabilities[featureId].save + : false; + const capabilitiesCanUserRead: boolean = + typeof uiCapabilities[featureId].show === 'boolean' + ? uiCapabilities[featureId].show + : false; + return { + crud: capabilitiesCanUserCRUD, + read: capabilitiesCanUserRead, + loading: false, + featureId, + }; + }); + } + }, [alertsPermissions.featureId, featureId, uiCapabilities]); + + return alertsPermissions; +}; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index 90032419948ef..c4d455fb43b7f 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -24,7 +24,6 @@ import type { ALERT_DURATION as ALERT_DURATION_TYPED, ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, ALERT_UUID as ALERT_UUID_TYPED, ALERT_RULE_CATEGORY as ALERT_RULE_CATEGORY_TYPED, ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, @@ -33,7 +32,6 @@ import { ALERT_DURATION as ALERT_DURATION_NON_TYPED, ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, ALERT_UUID as ALERT_UUID_NON_TYPED, ALERT_RULE_CATEGORY as ALERT_RULE_CATEGORY_NON_TYPED, ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, @@ -46,7 +44,6 @@ import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana import { asDuration } from '../../../../common/utils/formatters'; import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry'; import { parseAlert } from '../parse_alert'; -import { SeverityBadge } from '../severity_badge'; type AlertsFlyoutProps = { alert?: TopAlert; @@ -59,7 +56,6 @@ type AlertsFlyoutProps = { const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; const ALERT_UUID: typeof ALERT_UUID_TYPED = ALERT_UUID_NON_TYPED; const ALERT_RULE_CATEGORY: typeof ALERT_RULE_CATEGORY_TYPED = ALERT_RULE_CATEGORY_NON_TYPED; const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; @@ -97,14 +93,8 @@ export function AlertsFlyout({ description: alertData.active ? 'Active' : 'Recovered', }, { - title: i18n.translate('xpack.observability.alertsFlyout.severityLabel', { - defaultMessage: 'Severity', - }), - description: , - }, - { - title: i18n.translate('xpack.observability.alertsFlyout.triggeredLabel', { - defaultMessage: 'Triggered', + title: i18n.translate('xpack.observability.alertsFlyout.lastUpdatedLabel', { + defaultMessage: 'Last updated', }), description: ( {moment(alertData.start).format(dateFormat)} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx index 01cdfccbd05e5..f32088e2646b3 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx @@ -51,6 +51,7 @@ export function AlertsSearchBar({ }); setQueryLanguage((nextQuery?.language || 'kuery') as 'kuery' | 'lucene'); }} + displayStyle="inPage" /> ); } diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index afa6ba728b41f..bbbd81b4e49ea 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -10,18 +10,17 @@ * We have types and code at different imports because we don't want to import the whole package in the resulting webpack bundle for the plugin. * This way plugins can do targeted imports to reduce the final code bundle */ -import type { +import { AlertConsumers as AlertConsumersTyped, ALERT_DURATION as ALERT_DURATION_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, ALERT_STATUS as ALERT_STATUS_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, + ALERT_REASON as ALERT_REASON_TYPED, + ALERT_RULE_CONSUMER, } from '@kbn/rule-data-utils'; import { ALERT_DURATION as ALERT_DURATION_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, ALERT_STATUS as ALERT_STATUS_NON_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + ALERT_REASON as ALERT_REASON_NON_TYPED, TIMESTAMP, // @ts-expect-error importing from a place other than root because we want to limit what we import from this package } from '@kbn/rule-data-utils/target_node/technical_field_names'; @@ -40,8 +39,10 @@ import { import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import React, { Suspense, useMemo, useState, useCallback } from 'react'; - +import { get } from 'lodash'; +import { useGetUserAlertsPermissions } from '../../hooks/use_alert_permission'; import type { TimelinesUIStart, TGridType, SortDirection } from '../../../../timelines/public'; +import { useStatusBulkActionItems } from '../../../../timelines/public'; import type { TopAlert } from './'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import type { @@ -58,12 +59,12 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; import { getDefaultCellActions } from './default_cell_actions'; import { LazyAlertsFlyout } from '../..'; import { parseAlert } from './parse_alert'; +import { CoreStart } from '../../../../../../src/core/public'; const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; -const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; +const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; interface AlertsTableTGridProps { indexName: string; @@ -72,9 +73,11 @@ interface AlertsTableTGridProps { kuery: string; status: string; setRefetch: (ref: () => void) => void; + addToQuery: (value: string) => void; } interface ObservabilityActionsProps extends ActionProps { + currentStatus: AlertStatus; setFlyoutAlert: React.Dispatch>; } @@ -107,10 +110,10 @@ export const columns: Array< { columnHeaderType: 'not-filtered', displayAsText: i18n.translate('xpack.observability.alertsTGrid.statusColumnDescription', { - defaultMessage: 'Status', + defaultMessage: 'Alert Status', }), id: ALERT_STATUS, - initialWidth: 79, + initialWidth: 110, }, { columnHeaderType: 'not-filtered', @@ -128,21 +131,13 @@ export const columns: Array< id: ALERT_DURATION, initialWidth: 116, }, - { - columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.severityColumnDescription', { - defaultMessage: 'Severity', - }), - id: ALERT_SEVERITY_LEVEL, - initialWidth: 102, - }, { columnHeaderType: 'not-filtered', displayAsText: i18n.translate('xpack.observability.alertsTGrid.reasonColumnDescription', { defaultMessage: 'Reason', }), + id: ALERT_REASON, linkField: '*', - id: ALERT_RULE_NAME, }, ]; @@ -161,15 +156,27 @@ function ObservabilityActions({ data, eventId, ecsData, + currentStatus, + refetch, setFlyoutAlert, + setEventsLoading, + setEventsDeleted, }: ObservabilityActionsProps) { const { core, observabilityRuleTypeRegistry } = usePluginContext(); const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); const [openActionsPopoverId, setActionsPopover] = useState(null); - const { timelines } = useKibana<{ timelines: TimelinesUIStart }>().services; + const { + timelines, + application: { capabilities }, + } = useKibana().services; + const parseObservabilityAlert = useMemo(() => parseAlert(observabilityRuleTypeRegistry), [ observabilityRuleTypeRegistry, ]); + const alertDataConsumer = useMemo(() => get(dataFieldEs, ALERT_RULE_CONSUMER, [''])[0], [ + dataFieldEs, + ]); + const alert = parseObservabilityAlert(dataFieldEs); const { prepend } = core.http.basePath; @@ -181,8 +188,8 @@ function ObservabilityActions({ setActionsPopover(null); }, []); - const openActionsPopover = useCallback((id) => { - setActionsPopover(id); + const toggleActionsPopover = useCallback((id) => { + setActionsPopover((current) => (current ? null : id)); }, []); const casePermissions = useGetUserCasesPermissions(); const event = useMemo(() => { @@ -193,31 +200,48 @@ function ObservabilityActions({ }; }, [data, eventId, ecsData]); + const onAlertStatusUpdated = useCallback(() => { + setActionsPopover(null); + if (refetch) { + refetch(); + } + }, [setActionsPopover, refetch]); + + const alertPermissions = useGetUserAlertsPermissions(capabilities, alertDataConsumer); + + const statusActionItems = useStatusBulkActionItems({ + eventIds: [eventId], + currentStatus, + indexName: ecsData._index ?? '', + setEventsLoading, + setEventsDeleted, + onUpdateSuccess: onAlertStatusUpdated, + onUpdateFailure: onAlertStatusUpdated, + }); + const actionsPanels = useMemo(() => { return [ { id: 0, content: [ - <> - {timelines.getAddToExistingCaseButton({ - event, - casePermissions, - appId: observabilityFeatureId, - onClose: afterCaseSelection, - })} - , - <> - {timelines.getAddToNewCaseButton({ - event, - casePermissions, - appId: observabilityFeatureId, - onClose: afterCaseSelection, - })} - , + timelines.getAddToExistingCaseButton({ + event, + casePermissions, + appId: observabilityFeatureId, + onClose: afterCaseSelection, + }), + timelines.getAddToNewCaseButton({ + event, + casePermissions, + appId: observabilityFeatureId, + onClose: afterCaseSelection, + }), + ...(alertPermissions.crud ? statusActionItems : []), ], }, ]; - }, [afterCaseSelection, casePermissions, timelines, event]); + }, [afterCaseSelection, casePermissions, timelines, event, statusActionItems, alertPermissions]); + return ( <> @@ -247,7 +271,7 @@ function ObservabilityActions({ color="text" iconType="boxesHorizontal" aria-label="More" - onClick={() => openActionsPopover(eventId)} + onClick={() => toggleActionsPopover(eventId)} /> } isOpen={openActionsPopoverId === eventId} @@ -264,7 +288,7 @@ function ObservabilityActions({ } export function AlertsTableTGrid(props: AlertsTableTGridProps) { - const { indexName, rangeFrom, rangeTo, kuery, status, setRefetch } = props; + const { indexName, rangeFrom, rangeTo, kuery, status, setRefetch, addToQuery } = props; const { timelines } = useKibana<{ timelines: TimelinesUIStart }>().services; const [flyoutAlert, setFlyoutAlert] = useState(undefined); @@ -286,11 +310,17 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { ); }, rowCellRender: (actionProps: ActionProps) => { - return ; + return ( + + ); }, }, ]; - }, []); + }, [status]); const tGridProps = useMemo(() => { const type: TGridType = 'standalone'; @@ -302,7 +332,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { type, columns, deletedEventIds: [], - defaultCellActions: getDefaultCellActions({ enableFilterActions: false }), + defaultCellActions: getDefaultCellActions({ addToQuery }), end: rangeTo, filters: [], indexNames: [indexName], @@ -347,6 +377,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { rangeTo, setRefetch, status, + addToQuery, ]); const handleFlyoutClose = () => setFlyoutAlert(undefined); const { observabilityRuleTypeRegistry } = usePluginContext(); diff --git a/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx index 3056b026fc27a..7e166ac99c05f 100644 --- a/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx @@ -6,16 +6,18 @@ */ import React from 'react'; - +import { i18n } from '@kbn/i18n'; import { ObservabilityPublicPluginsStart } from '../..'; import { getMappedNonEcsValue } from './render_cell_value'; +import FilterForValueButton from './filter_for_value'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { TimelineNonEcsData } from '../../../../timelines/common/search_strategy'; import { TGridCellAction } from '../../../../timelines/common/types/timeline'; import { TimelinesUIStart } from '../../../../timelines/public'; -/** a noop required by the filter in / out buttons */ -const onFilterAdded = () => {}; +export const FILTER_FOR_VALUE = i18n.translate('xpack.observability.hoverActions.filterForValue', { + defaultMessage: 'Filter for value', +}); /** a hook to eliminate the verbose boilerplate required to use common services */ const useKibanaServices = () => { @@ -31,32 +33,10 @@ const useKibanaServices = () => { return { timelines, filterManager }; }; -/** actions for adding filters to the search bar */ -const filterCellActions: TGridCellAction[] = [ - ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { - const { timelines, filterManager } = useKibanaServices(); - - const value = getMappedNonEcsValue({ - data: data[rowIndex], - fieldName: columnId, - }); - - return ( - <> - {timelines.getHoverActions().getFilterForValueButton({ - Component, - field: columnId, - filterManager, - onFilterAdded, - ownFocus: false, - showTooltip: false, - value, - })} - - ); - }, +/** actions common to all cells (e.g. copy to clipboard) */ +const commonCellActions: TGridCellAction[] = [ ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { - const { timelines, filterManager } = useKibanaServices(); + const { timelines } = useKibanaServices(); const value = getMappedNonEcsValue({ data: data[rowIndex], @@ -65,11 +45,10 @@ const filterCellActions: TGridCellAction[] = [ return ( <> - {timelines.getHoverActions().getFilterOutValueButton({ + {timelines.getHoverActions().getCopyButton({ Component, field: columnId, - filterManager, - onFilterAdded, + isHoverAction: false, ownFocus: false, showTooltip: false, value, @@ -79,31 +58,27 @@ const filterCellActions: TGridCellAction[] = [ }, ]; -/** actions common to all cells (e.g. copy to clipboard) */ -const commonCellActions: TGridCellAction[] = [ +/** actions for adding filters to the search bar */ +const buildFilterCellActions = (addToQuery: (value: string) => void): TGridCellAction[] => [ ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { - const { timelines } = useKibanaServices(); - const value = getMappedNonEcsValue({ data: data[rowIndex], fieldName: columnId, }); return ( - <> - {timelines.getHoverActions().getCopyButton({ - Component, - field: columnId, - isHoverAction: false, - ownFocus: false, - showTooltip: false, - value, - })} - + ); }, ]; /** returns the default actions shown in `EuiDataGrid` cells */ -export const getDefaultCellActions = ({ enableFilterActions }: { enableFilterActions: boolean }) => - enableFilterActions ? [...filterCellActions, ...commonCellActions] : [...commonCellActions]; +export const getDefaultCellActions = ({ addToQuery }: { addToQuery: (value: string) => void }) => [ + ...buildFilterCellActions(addToQuery), + ...commonCellActions, +]; diff --git a/x-pack/plugins/observability/public/pages/alerts/example_data.ts b/x-pack/plugins/observability/public/pages/alerts/example_data.ts index 112932d49311c..535556a9b6ec1 100644 --- a/x-pack/plugins/observability/public/pages/alerts/example_data.ts +++ b/x-pack/plugins/observability/public/pages/alerts/example_data.ts @@ -9,8 +9,7 @@ import { ALERT_DURATION, ALERT_END, ALERT_ID, - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, + ALERT_SEVERITY, ALERT_RULE_TYPE_ID, ALERT_START, ALERT_STATUS, @@ -28,7 +27,7 @@ export const apmAlertResponseExample = [ [ALERT_RULE_NAME]: ['Error count threshold | opbeans-java (smith test)'], [ALERT_DURATION]: [180057000], [ALERT_STATUS]: ['open'], - [ALERT_SEVERITY_LEVEL]: ['warning'], + [ALERT_SEVERITY]: ['warning'], tags: ['apm', 'service.name:opbeans-java'], [ALERT_UUID]: ['0175ec0a-a3b1-4d41-b557-e21c2d024352'], [ALERT_RULE_UUID]: ['474920d0-93e9-11eb-ac86-0b455460de81'], @@ -123,21 +122,13 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: ALERT_SEVERITY_LEVEL, + name: ALERT_SEVERITY, type: 'string', esTypes: ['keyword'], searchable: true, aggregatable: true, readFromDocValues: true, }, - { - name: ALERT_SEVERITY_VALUE, - type: 'number', - esTypes: ['long'], - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, { name: ALERT_START, type: 'date', diff --git a/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx b/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx new file mode 100644 index 0000000000000..77cac9d482a37 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; + +export const filterForValueButtonLabel = i18n.translate( + 'xpack.observability.hoverActions.filterForValueButtonLabel', + { + defaultMessage: 'Filter for value', + } +); + +import { EuiButtonIcon, EuiButtonEmpty } from '@elastic/eui'; + +interface FilterForValueProps { + Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon; + field: string; + value: string[] | string | null | undefined; + addToQuery: (value: string) => void; +} + +const FilterForValueButton: React.FC = React.memo( + ({ Component, field, value, addToQuery }) => { + const text = useMemo(() => `${field}${value != null ? `: "${value}"` : ''}`, [field, value]); + const onClick = useCallback(() => { + addToQuery(text); + }, [text, addToQuery]); + const button = useMemo( + () => + Component ? ( + + {filterForValueButtonLabel} + + ) : ( + + ), + [Component, onClick] + ); + return button; + } +); + +FilterForValueButton.displayName = 'FilterForValueButton'; + +// eslint-disable-next-line import/no-default-export +export { FilterForValueButton as default }; diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index 51d5f52bfea87..b3ff3f94dc4db 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -5,14 +5,7 @@ * 2.0. */ -import { - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo, useRef } from 'react'; import { useHistory } from 'react-router-dom'; @@ -47,7 +40,12 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { const history = useHistory(); const refetch = useRef<() => void>(); const { - query: { rangeFrom = 'now-15m', rangeTo = 'now', kuery = '', status = 'open' }, + query: { + rangeFrom = 'now-15m', + rangeTo = 'now', + kuery = 'kibana.alert.status: "open"', // TODO change hardcoded values as part of another PR + status = 'open', + }, } = routeParams; useBreadcrumbs([ @@ -105,6 +103,20 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { [history, rangeFrom, rangeTo, kuery] ); + const addToQuery = useCallback( + (value: string) => { + let output = value; + if (kuery !== '') { + output = `${kuery} and ${value}`; + } + onQueryChange({ + dateRange: { from: rangeFrom, to: rangeTo }, + query: output, + }); + }, + [kuery, onQueryChange, rangeFrom, rangeTo] + ); + const setRefetch = useCallback((ref) => { refetch.current = ref; }, []); @@ -127,7 +139,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { ], }} > - + - - - - - - - - - - - 0 ? dynamicIndexPattern[0].title : ''} - rangeFrom={rangeFrom} - rangeTo={rangeTo} - kuery={kuery} - status={status} - setRefetch={setRefetch} - /> - - + + + + + + + + + + + 0 ? dynamicIndexPattern[0].title : ''} + rangeFrom={rangeFrom} + rangeTo={rangeTo} + kuery={kuery} + status={status} + setRefetch={setRefetch} + addToQuery={addToQuery} + /> + + + ); diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index f6e1d41c2a6f9..c85ea0b1086fa 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiIconTip, EuiLink } from '@elastic/eui'; +import { EuiLink, EuiHealth, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; /** @@ -14,15 +14,15 @@ import React, { useEffect } from 'react'; */ import type { ALERT_DURATION as ALERT_DURATION_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_TYPED, ALERT_STATUS as ALERT_STATUS_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, + ALERT_REASON as ALERT_REASON_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_DURATION as ALERT_DURATION_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, ALERT_STATUS as ALERT_STATUS_NON_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + ALERT_REASON as ALERT_REASON_NON_TYPED, TIMESTAMP, // @ts-expect-error importing from a place other than root because we want to limit what we import from this package } from '@kbn/rule-data-utils/target_node/technical_field_names'; @@ -34,11 +34,12 @@ import { SeverityBadge } from './severity_badge'; import { TopAlert } from '.'; import { parseAlert } from './parse_alert'; import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useTheme } from '../../hooks/use_theme'; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; -const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; +const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; export const getMappedNonEcsValue = ({ data, @@ -86,31 +87,40 @@ export const getRenderCellValue = ({ } }, [columnId, setCellProps]); + const theme = useTheme(); + switch (columnId) { case ALERT_STATUS: - return value !== 'closed' ? ( - - ) : ( - - ); + switch (value) { + case 'open': + return ( + + {i18n.translate('xpack.observability.alertsTGrid.statusActiveDescription', { + defaultMessage: 'Active', + })} + + ); + case 'closed': + return ( + + + {i18n.translate('xpack.observability.alertsTGrid.statusRecoveredDescription', { + defaultMessage: 'Recovered', + })} + + + ); + default: + // NOTE: This fallback shouldn't be needed. Status should be either "active" or "recovered". + return null; + } case TIMESTAMP: return ; case ALERT_DURATION: return asDuration(Number(value)); - case ALERT_SEVERITY_LEVEL: + case ALERT_SEVERITY: return ; - case ALERT_RULE_NAME: + case ALERT_REASON: const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); const alert = parseAlert(observabilityRuleTypeRegistry)(dataFieldEs); diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index f51f76a395199..7d11050f14d15 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -24,6 +24,7 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; +import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import type { HomePublicPluginSetup, HomePublicPluginStart, @@ -52,6 +53,7 @@ export interface ObservabilityPublicPluginsSetup { export interface ObservabilityPublicPluginsStart { cases: CasesUiStart; + embeddable: EmbeddableStart; home?: HomePublicPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; diff --git a/x-pack/plugins/osquery/kibana.json b/x-pack/plugins/osquery/kibana.json index f947866641c28..a499b2b75ee68 100644 --- a/x-pack/plugins/osquery/kibana.json +++ b/x-pack/plugins/osquery/kibana.json @@ -1,25 +1,14 @@ { - "configPath": [ - "xpack", - "osquery" - ], - "extraPublicDirs": [ - "common" - ], + "configPath": ["xpack", "osquery"], + "extraPublicDirs": ["common"], "id": "osquery", + "owner": { + "name": "Security asset management", + "githubTeam": "security-asset-management" + }, "kibanaVersion": "kibana", - "optionalPlugins": [ - "fleet", - "home", - "usageCollection", - "lens" - ], - "requiredBundles": [ - "esUiShared", - "fleet", - "kibanaUtils", - "kibanaReact" - ], + "optionalPlugins": ["fleet", "home", "usageCollection", "lens"], + "requiredBundles": ["esUiShared", "fleet", "kibanaUtils", "kibanaReact"], "requiredPlugins": [ "actions", "data", diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx index ae3ddb1c0b861..1ab87949e3493 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx @@ -167,7 +167,7 @@ const ViewResultsInLensActionComponent: React.FC { - const openInNewWindow = !(!isModifiedEvent(event) && isLeftClickEvent(event)); + const openInNewTab = !(!isModifiedEvent(event) && isLeftClickEvent(event)); event.preventDefault(); @@ -181,7 +181,9 @@ const ViewResultsInLensActionComponent: React.FC base as TaskPayloadPNG & TaskPayloadPDF; test(`fails if no URL is passed`, async () => { const fn = () => getFullUrls(mockConfig, getMockJob({})); expect(fn).toThrowErrorMatchingInlineSnapshot( - `"No valid URL fields found in Job Params! Expected \`job.relativeUrl: string\` or \`job.relativeUrls: string[]\`"` + `"No valid URL fields found in Job Params! Expected \`job.relativeUrl\` or \`job.objects[{ relativeUrl }]\`"` ); }); @@ -54,14 +54,7 @@ test(`fails if URLs are absolute for PNGs`, async () => { test(`fails if URLs are file-protocols for PDF`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const relativeUrl = 'file://etc/passwd/#/something'; - const fn = () => - getFullUrls( - mockConfig, - getMockJob({ - relativeUrls: [relativeUrl], - forceNow, - }) - ); + const fn = () => getFullUrls(mockConfig, getMockJob({ objects: [{ relativeUrl }], forceNow })); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: file://etc/passwd/#/something"` ); @@ -75,7 +68,7 @@ test(`fails if URLs are absolute for PDF`, async () => { getFullUrls( mockConfig, getMockJob({ - relativeUrls: [relativeUrl], + objects: [{ relativeUrl }], forceNow, }) ); @@ -86,13 +79,16 @@ test(`fails if URLs are absolute for PDF`, async () => { test(`fails if any URLs are absolute or file's for PDF`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const relativeUrls = [ - '/app/kibana#/something_aaa', - 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something', - 'file://etc/passwd/#/something', + const objects = [ + { relativeUrl: '/app/kibana#/something_aaa' }, + { + relativeUrl: + 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something', + }, + { relativeUrl: 'file://etc/passwd/#/something' }, ]; - const fn = () => getFullUrls(mockConfig, getMockJob({ relativeUrls, forceNow })); + const fn = () => getFullUrls(mockConfig, getMockJob({ objects, forceNow })); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something file://etc/passwd/#/something"` ); @@ -107,7 +103,7 @@ test(`fails if URL does not route to a visualization`, async () => { test(`adds forceNow to hash's query, if it exists`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const urls = await getFullUrls( + const urls = getFullUrls( mockConfig, getMockJob({ relativeUrl: '/app/kibana#/something', forceNow }) ); @@ -120,7 +116,7 @@ test(`adds forceNow to hash's query, if it exists`, async () => { test(`appends forceNow to hash's query, if it exists`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const urls = await getFullUrls( + const urls = getFullUrls( mockConfig, getMockJob({ relativeUrl: '/app/kibana#/something?_g=something', forceNow }) ); @@ -131,21 +127,21 @@ test(`appends forceNow to hash's query, if it exists`, async () => { }); test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { - const urls = await getFullUrls(mockConfig, getMockJob({ relativeUrl: '/app/kibana#/something' })); + const urls = getFullUrls(mockConfig, getMockJob({ relativeUrl: '/app/kibana#/something' })); expect(urls[0]).toEqual('http://localhost:5601/sbp/app/kibana#/something'); }); test(`adds forceNow to each of multiple urls`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const urls = await getFullUrls( + const urls = getFullUrls( mockConfig, getMockJob({ - relativeUrls: [ - '/app/kibana#/something_aaa', - '/app/kibana#/something_bbb', - '/app/kibana#/something_ccc', - '/app/kibana#/something_ddd', + objects: [ + { relativeUrl: '/app/kibana#/something_aaa' }, + { relativeUrl: '/app/kibana#/something_bbb' }, + { relativeUrl: '/app/kibana#/something_ccc' }, + { relativeUrl: '/app/kibana#/something_ddd' }, ], forceNow, }) 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 0fe1298932b36..5ae4092a466fa 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 @@ -21,7 +21,7 @@ function isPngJob(job: TaskPayloadPNG | TaskPayloadPDF): job is TaskPayloadPNG { return (job as TaskPayloadPNG).relativeUrl !== undefined; } function isPdfJob(job: TaskPayloadPNG | TaskPayloadPDF): job is TaskPayloadPDF { - return (job as TaskPayloadPDF).relativeUrls !== undefined; + return (job as TaskPayloadPDF).objects !== undefined; } export function getFullUrls(config: ReportingConfig, job: TaskPayloadPDF | TaskPayloadPNG) { @@ -39,17 +39,17 @@ export function getFullUrls(config: ReportingConfig, job: TaskPayloadPDF | TaskP if (isPngJob(job)) { relativeUrls = [job.relativeUrl]; } else if (isPdfJob(job)) { - relativeUrls = job.relativeUrls; + relativeUrls = job.objects.map((obj) => obj.relativeUrl); } else { throw new Error( - `No valid URL fields found in Job Params! Expected \`job.relativeUrl: string\` or \`job.relativeUrls: string[]\`` + `No valid URL fields found in Job Params! Expected \`job.relativeUrl\` or \`job.objects[{ relativeUrl }]\`` ); } validateUrls(relativeUrls); const urls = relativeUrls.map((relativeUrl) => { - const parsedRelative: UrlWithStringQuery = urlParse(relativeUrl); + const parsedRelative: UrlWithStringQuery = urlParse(relativeUrl); // FIXME: '(urlStr: string): UrlWithStringQuery' is deprecated const jobUrl = getAbsoluteUrl({ path: parsedRelative.pathname === null ? undefined : parsedRelative.pathname, hash: parsedRelative.hash === null ? undefined : parsedRelative.hash, diff --git a/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts b/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts index 22e0fc7de8455..e6d392c0bb55c 100644 --- a/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts @@ -22,6 +22,15 @@ const isBogusUrl = (url: string) => { }; export const validateUrls = (urls: string[]): void => { + if (!Array.isArray(urls)) { + throw new Error('Invalid relativeUrls. String[] is expected.'); + } + urls.forEach((url) => { + if (typeof url !== 'string') { + throw new Error('Invalid Relative URL in relativeUrls. String is expected.'); + } + }); + const badUrls = filter(urls, (url) => isBogusUrl(url)); if (badUrls.length) { 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 a2f58e5835f22..aae2e85e823db 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 @@ -22,11 +22,10 @@ export const createJobFnFactory: CreateJobFnFactory< validateUrls([jobParams.relativeUrl]); return { - isDeprecated: true, + ...jobParams, headers: serializedEncryptedHeaders, spaceId: reporting.getSpaceId(req, logger), forceNow: new Date().toISOString(), - ...jobParams, }; }; }; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts new file mode 100644 index 0000000000000..4e366f506af2d --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { createMockLevelLogger } from '../../../test_helpers'; +import { compatibilityShim } from './compatibility_shim'; + +const mockRequestHandlerContext = { + core: coreMock.createRequestHandlerContext(), + reporting: { usesUiCapabilities: () => true }, +}; +const mockLogger = createMockLevelLogger(); + +const mockKibanaRequest = httpServerMock.createKibanaRequest(); +const createMockSavedObject = (body: any) => ({ + id: 'mockSavedObjectId123', + type: 'mockSavedObjectType', + references: [], + ...body, +}); +const createMockJobParams = (body: any) => ({ + ...body, +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +test(`passes title through if provided`, async () => { + const title = 'test title'; + + const createJobMock = jest.fn(); + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ title, relativeUrls: ['/something'] }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); + + expect(createJobMock.mock.calls.length).toBe(1); + expect(createJobMock.mock.calls[0][0].title).toBe(title); +}); + +test(`gets the title from the savedObject`, async () => { + const createJobMock = jest.fn(); + const title = 'savedTitle'; + mockRequestHandlerContext.core.savedObjects.client.get.mockResolvedValue( + createMockSavedObject({ + attributes: { title }, + }) + ); + + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ objectType: 'search', savedObjectId: 'abc' }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(2); + expect(mockLogger.warn.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.error.mock.calls.length).toBe(0); + + expect(createJobMock.mock.calls.length).toBe(1); + expect(createJobMock.mock.calls[0][0].title).toBe(title); +}); + +test(`passes the objectType and savedObjectId to the savedObjectsClient`, async () => { + const createJobMock = jest.fn(); + const context = mockRequestHandlerContext; + context.core.savedObjects.client.get.mockResolvedValue( + createMockSavedObject({ attributes: { title: '' } }) + ); + + const objectType = 'search'; + const savedObjectId = 'abc'; + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ objectType, savedObjectId }), + context, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(2); + expect(mockLogger.warn.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.warn.mock.calls[1][0]).toEqual( + 'The title has been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.error.mock.calls.length).toBe(0); + + const getMock = context.core.savedObjects.client.get.mock; + expect(getMock.calls.length).toBe(1); + expect(getMock.calls[0][0]).toBe(objectType); + expect(getMock.calls[0][1]).toBe(savedObjectId); +}); + +test(`logs no warnings when title and relativeUrls is passed`, async () => { + const createJobMock = jest.fn(); + + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ title: 'Phenomenal Dashboard', relativeUrls: ['/abc', '/def'] }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); +}); + +test(`logs warning if title can not be provided`, async () => { + const createJobMock = jest.fn(); + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ relativeUrls: ['/abc'] }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(1); + expect(mockLogger.warn.mock.calls[0][0]).toEqual( + `A title parameter should be provided with the job generation request. Please ` + + `use Kibana to regenerate your POST URL to have a title included in the PDF.` + ); +}); + +test(`logs deprecations when generating the title/relativeUrl using the savedObject`, async () => { + const createJobMock = jest.fn(); + mockRequestHandlerContext.core.savedObjects.client.get.mockResolvedValue( + createMockSavedObject({ + attributes: { title: '' }, + }) + ); + + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ objectType: 'search', savedObjectId: 'abc' }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(2); + expect(mockLogger.warn.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.warn.mock.calls[1][0]).toEqual( + 'The title has been derived from saved object parameters. This functionality will be removed with the next major version.' + ); +}); + +test(`passes objectType through`, async () => { + const createJobMock = jest.fn(); + + const objectType = 'foo'; + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ title: 'test', relativeUrls: ['/something'], objectType }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); + + expect(createJobMock.mock.calls.length).toBe(1); + expect(createJobMock.mock.calls[0][0].objectType).toBe(objectType); +}); + +test(`passes the relativeUrls through`, async () => { + const createJobMock = jest.fn(); + + const relativeUrls = ['/app/kibana#something', '/app/kibana#something-else']; + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ title: 'test', relativeUrls }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); + + expect(createJobMock.mock.calls.length).toBe(1); + expect(createJobMock.mock.calls[0][0].relativeUrls).toBe(relativeUrls); +}); + +const testSavedObjectRelativeUrl = (objectType: string, expectedUrl: string) => { + test(`generates the saved object relativeUrl for ${objectType}`, async () => { + const createJobMock = jest.fn(); + + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ title: 'test', objectType, savedObjectId: 'abc' }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(1); + expect(mockLogger.warn.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.error.mock.calls.length).toBe(0); + + expect(createJobMock.mock.calls.length).toBe(1); + expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual([expectedUrl]); + }); +}; + +testSavedObjectRelativeUrl('search', '/app/kibana#/discover/abc?'); +testSavedObjectRelativeUrl('visualization', '/app/kibana#/visualize/edit/abc?'); +testSavedObjectRelativeUrl('dashboard', '/app/kibana#/dashboard/abc?'); + +test(`appends the queryString to the relativeUrl when generating from the savedObject`, async () => { + const createJobMock = jest.fn(); + + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ + title: 'test', + objectType: 'search', + savedObjectId: 'abc', + queryString: 'foo=bar', + }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(1); + expect(mockLogger.warn.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.error.mock.calls.length).toBe(0); + + expect(createJobMock.mock.calls.length).toBe(1); + expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual([ + '/app/kibana#/discover/abc?foo=bar', + ]); +}); + +test(`throw an Error if the objectType, savedObjectId and relativeUrls are provided`, async () => { + const createJobMock = jest.fn(); + + const promise = compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ + title: 'test', + objectType: 'something', + relativeUrls: ['/something'], + savedObjectId: 'abc', + }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + await expect(promise).rejects.toBeDefined(); +}); + +test(`passes headers and request through`, async () => { + const createJobMock = jest.fn(); + + await compatibilityShim(createJobMock, mockLogger)( + createMockJobParams({ title: 'test', relativeUrls: ['/something'] }), + mockRequestHandlerContext, + mockKibanaRequest + ); + + expect(mockLogger.warn.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); + + expect(createJobMock.mock.calls.length).toBe(1); + expect(createJobMock.mock.calls[0][1]).toBe(mockRequestHandlerContext); + expect(createJobMock.mock.calls[0][2]).toBe(mockKibanaRequest); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts new file mode 100644 index 0000000000000..f806b8a7e5bca --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest } from 'kibana/server'; +import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/server'; +import type { LevelLogger } from '../../../lib'; +import type { CreateJobFn, ReportingRequestHandlerContext } from '../../../types'; +import type { JobParamsPDF, JobParamsPDFLegacy, TaskPayloadPDF } from '../types'; + +function isLegacyJob( + jobParams: JobParamsPDF | JobParamsPDFLegacy +): jobParams is JobParamsPDFLegacy { + return (jobParams as JobParamsPDFLegacy).savedObjectId != null; +} + +const getSavedObjectTitle = async ( + objectType: string, + savedObjectId: string, + savedObjectsClient: any +) => { + const savedObject = await savedObjectsClient.get(objectType, savedObjectId); + return savedObject.attributes.title; +}; + +const getSavedObjectRelativeUrl = ( + objectType: string, + savedObjectId: string, + queryString: string +) => { + const appPrefixes: Record = { + dashboard: '/dashboard/', + visualization: '/visualize/edit/', + search: '/discover/', + }; + + const appPrefix = appPrefixes[objectType]; + if (!appPrefix) throw new Error('Unexpected app type: ' + objectType); + + const hash = appPrefix + urlUtils.encodeUriQuery(savedObjectId, true); + + return `/app/kibana#${hash}?${queryString || ''}`; +}; + +/* + * The compatibility shim is responsible for migrating an older shape of the + * PDF Job Params into a newer shape, by deriving a report title and relative + * URL from a savedObjectId and queryString. + */ +export function compatibilityShim( + createJobFn: CreateJobFn, + logger: LevelLogger +) { + return async function ( + jobParams: JobParamsPDF | JobParamsPDFLegacy, + context: ReportingRequestHandlerContext, + req: KibanaRequest + ) { + let kibanaRelativeUrls = (jobParams as JobParamsPDF).relativeUrls; + let reportTitle = jobParams.title; + let isDeprecated = false; + + if ( + (jobParams as JobParamsPDFLegacy).savedObjectId && + (jobParams as JobParamsPDF).relativeUrls + ) { + throw new Error(`savedObjectId should not be provided if relativeUrls are provided`); + } + + if (isLegacyJob(jobParams)) { + const { savedObjectId, objectType, queryString } = jobParams; + + // input validation and deprecation logging + if (typeof savedObjectId !== 'string') { + throw new Error('Invalid savedObjectId (deprecated). String is expected.'); + } + if (typeof objectType !== 'string') { + throw new Error('Invalid objectType (deprecated). String is expected.'); + } + + // legacy parameters need to be converted into a relative URL + kibanaRelativeUrls = [getSavedObjectRelativeUrl(objectType, savedObjectId, queryString)]; + logger.warn( + `The relativeUrls have been derived from saved object parameters. ` + + `This functionality will be removed with the next major version.` + ); + + // legacy parameters might need to get the title from the saved object + if (!reportTitle) { + try { + reportTitle = await getSavedObjectTitle( + objectType, + savedObjectId, + context.core.savedObjects.client + ); + logger.warn( + `The title has been derived from saved object parameters. This ` + + `functionality will be removed with the next major version.` + ); + } catch (err) { + logger.error(err); // 404 for the savedObjectId, etc + throw err; + } + } + + isDeprecated = true; + } + + if (typeof reportTitle !== 'string') { + logger.warn( + `A title parameter should be provided with the job generation ` + + `request. Please use Kibana to regenerate your POST URL to have a ` + + `title included in the PDF.` + ); + reportTitle = ''; + } + + const transformedJobParams: JobParamsPDF = { + ...jobParams, + title: reportTitle, + relativeUrls: kibanaRelativeUrls, + isDeprecated, // tack on this flag so it will be saved the TaskPayload + }; + + return await createJobFn(transformedJobParams, context, req); + }; +} 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 0757b8f5745ff..8411dbcb94d11 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 @@ -5,28 +5,39 @@ * 2.0. */ +import { KibanaRequest } from 'src/core/server'; import { cryptoFactory } from '../../../lib'; -import { CreateJobFn, CreateJobFnFactory } from '../../../types'; +import { CreateJobFn, CreateJobFnFactory, ReportingRequestHandlerContext } from '../../../types'; import { validateUrls } from '../../common'; -import { JobParamsPDF, TaskPayloadPDF } from '../types'; +import { JobParamsPDF, JobParamsPDFLegacy, TaskPayloadPDF } from '../types'; +import { compatibilityShim } from './compatibility_shim'; +/* + * Incoming job params can be `JobParamsPDF` or `JobParamsPDFLegacy` depending + * on the version that the POST URL was copied from. + */ export const createJobFnFactory: CreateJobFnFactory< - CreateJobFn + CreateJobFn > = function createJobFactoryFn(reporting, logger) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function createJob(jobParams, _context, req) { - const serializedEncryptedHeaders = await crypto.encrypt(req.headers); + return compatibilityShim(async function createJobFn( + { relativeUrls, ...jobParams }: JobParamsPDF, // relativeUrls does not belong in the payload + _context: ReportingRequestHandlerContext, + req: KibanaRequest + ) { + validateUrls(relativeUrls); - validateUrls(jobParams.relativeUrls); + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); + // return the payload return { - isDeprecated: true, + ...jobParams, headers: serializedEncryptedHeaders, spaceId: reporting.getSpaceId(req, logger), forceNow: new Date().toISOString(), - ...jobParams, + objects: relativeUrls.map((u) => ({ relativeUrl: u })), }; - }; + }, logger); }; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index d481cd17bd911..98c00287aa196 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -7,8 +7,8 @@ jest.mock('../lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() })); -import { Writable } from 'stream'; import * as Rx from 'rxjs'; +import { Writable } from 'stream'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; @@ -95,7 +95,7 @@ test(`returns content_type of application/pdf`, async () => { const { content_type: contentType } = await runTask( 'pdfJobId', - getBasePayload({ relativeUrls: [], headers: encryptedHeaders }), + getBasePayload({ objects: [], headers: encryptedHeaders }), cancellationToken, stream ); @@ -111,7 +111,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const encryptedHeaders = await encryptHeaders({}); await runTask( 'pdfJobId', - getBasePayload({ relativeUrls: [], headers: encryptedHeaders }), + getBasePayload({ objects: [], headers: encryptedHeaders }), cancellationToken, stream ); 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 5172bf300abc8..8e4c45ad79506 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 @@ -10,13 +10,22 @@ import { BaseParams, BasePayload } from '../../types'; interface BaseParamsPDF { layout: LayoutParams; - forceNow?: string; - // TODO: Add comment explaining this field relativeUrls: string[]; + isDeprecated?: boolean; } // Job params: structure of incoming user request data, after being parsed from RISON export type JobParamsPDF = BaseParamsPDF & BaseParams; // Job payload: structure of stored job data provided by create_job -export type TaskPayloadPDF = BaseParamsPDF & BasePayload; +export interface TaskPayloadPDF extends BasePayload { + layout: LayoutParams; + forceNow?: string; + objects: Array<{ relativeUrl: string }>; +} + +type Legacy = Omit; +export interface JobParamsPDFLegacy extends Legacy { + savedObjectId: string; + queryString: string; +} diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts index 4082084c82fbc..adbfbda727af2 100644 --- a/x-pack/plugins/reporting/server/routes/generation.ts +++ b/x-pack/plugins/reporting/server/routes/generation.ts @@ -11,8 +11,9 @@ import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { LevelLogger as Logger } from '../lib'; import { enqueueJob } from '../lib/enqueue_job'; -import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate'; +import { registerGenerateFromJobParams } from './generate_from_jobparams'; +import { registerLegacy } from './legacy'; import { HandlerFunction } from './types'; const getDownloadBaseUrl = (reporting: ReportingCore) => { @@ -87,4 +88,5 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo registerGenerateFromJobParams(reporting, handler, handleError); registerGenerateCsvFromSavedObjectImmediate(reporting, handleError, logger); + registerLegacy(reporting, handler, handleError, logger); } diff --git a/x-pack/plugins/reporting/server/routes/legacy.ts b/x-pack/plugins/reporting/server/routes/legacy.ts new file mode 100644 index 0000000000000..79f1b7f17c2da --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/legacy.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import querystring from 'querystring'; +import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; +import { API_BASE_URL } from '../../common/constants'; +import { HandlerErrorFunction, HandlerFunction } from './types'; +import { ReportingCore } from '../core'; +import { LevelLogger } from '../lib'; + +const BASE_GENERATE = `${API_BASE_URL}/generate`; + +export function registerLegacy( + reporting: ReportingCore, + handler: HandlerFunction, + handleError: HandlerErrorFunction, + logger: LevelLogger +) { + const { router } = reporting.getPluginSetupDeps(); + + function createLegacyPdfRoute({ path, objectType }: { path: string; objectType: string }) { + const exportTypeId = 'printablePdf'; + + router.post( + { + path, + validate: { + params: schema.object({ + savedObjectId: schema.string({ minLength: 3 }), + }), + query: schema.any(), + }, + }, + + authorizedUserPreRouting(reporting, async (user, context, req, res) => { + const message = `The following URL is deprecated and will stop working in the next major version: ${req.url.pathname}${req.url.search}`; + logger.warn(message, ['deprecation']); + + try { + const { + title, + savedObjectId, + browserTimezone, + }: { title: string; savedObjectId: string; browserTimezone: string } = req.params as any; + const queryString = querystring.stringify(req.query as any); + + return await handler( + user, + exportTypeId, + { + title, + objectType, + savedObjectId, + browserTimezone, + queryString, + version: reporting.getKibanaVersion(), + }, + context, + req, + res + ); + } catch (err) { + throw handleError(res, err); + } + }) + ); + } + + createLegacyPdfRoute({ + path: `${BASE_GENERATE}/visualization/{savedId}`, + objectType: 'visualization', + }); + + createLegacyPdfRoute({ + path: `${BASE_GENERATE}/search/{savedId}`, + objectType: 'search', + }); + + createLegacyPdfRoute({ + path: `${BASE_GENERATE}/dashboard/{savedId}`, + objectType: 'dashboard', + }); +} diff --git a/x-pack/plugins/reporting/server/routes/types.d.ts b/x-pack/plugins/reporting/server/routes/types.d.ts index 2e8e94eaf265a..336605e6ff9b9 100644 --- a/x-pack/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/plugins/reporting/server/routes/types.d.ts @@ -8,15 +8,16 @@ import { KibanaRequest, KibanaResponseFactory } from 'src/core/server'; import type { BaseParams, + BaseParamsLegacyPDF, BasePayload, - ReportingUser, ReportingRequestHandlerContext, + ReportingUser, } from '../types'; export type HandlerFunction = ( user: ReportingUser, exportType: string, - jobParams: BaseParams, + jobParams: BaseParams | BaseParamsLegacyPDF, context: ReportingRequestHandlerContext, req: KibanaRequest, res: KibanaResponseFactory diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index 10541d9a4ebdd..20f284686f3b5 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -4,23 +4,12 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "management", - "licensing", - "features" - ], - "optionalPlugins": [ - "home", - "indexManagement", - "usageCollection", - "visTypeTimeseries" - ], + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, + "requiredPlugins": ["management", "licensing", "features"], + "optionalPlugins": ["home", "indexManagement", "usageCollection", "visTypeTimeseries"], "configPath": ["xpack", "rollup"], - "requiredBundles": [ - "kibanaUtils", - "kibanaReact", - "home", - "esUiShared", - "data" - ] + "requiredBundles": ["kibanaUtils", "kibanaReact", "home", "esUiShared", "data"] } diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index eb8d88cf697b9..b4ae89b7694f7 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -25,8 +25,7 @@ export const technicalRuleFieldMap = { [Fields.ALERT_START]: { type: 'date' }, [Fields.ALERT_END]: { type: 'date' }, [Fields.ALERT_DURATION]: { type: 'long' }, - [Fields.ALERT_SEVERITY_LEVEL]: { type: 'keyword' }, - [Fields.ALERT_SEVERITY_VALUE]: { type: 'long' }, + [Fields.ALERT_SEVERITY]: { type: 'keyword' }, [Fields.ALERT_STATUS]: { type: 'keyword' }, [Fields.ALERT_EVALUATION_THRESHOLD]: { type: 'scaled_float', scaling_factor: 100 }, [Fields.ALERT_EVALUATION_VALUE]: { type: 'scaled_float', scaling_factor: 100 }, @@ -95,11 +94,6 @@ export const technicalRuleFieldMap = { array: false, required: false, }, - [Fields.ALERT_RULE_ID]: { - type: 'keyword', - array: false, - required: false, - }, [Fields.ALERT_RULE_CREATED_AT]: { type: 'date', array: false, diff --git a/x-pack/plugins/rule_registry/common/types.ts b/x-pack/plugins/rule_registry/common/types.ts index cc23469524a4e..7b2fde48057a6 100644 --- a/x-pack/plugins/rule_registry/common/types.ts +++ b/x-pack/plugins/rule_registry/common/types.ts @@ -7,6 +7,270 @@ import { estypes } from '@elastic/elasticsearch'; +import * as t from 'io-ts'; + +// note: these schemas are not exhaustive. See the `Sort` type of `@elastic/elasticsearch` if you need to enhance it. +const fieldSchema = t.string; +export const sortOrderSchema = t.union([t.literal('asc'), t.literal('desc'), t.literal('_doc')]); +type SortOrderSchema = 'asc' | 'desc' | '_doc'; +const sortModeSchema = t.union([ + t.literal('min'), + t.literal('max'), + t.literal('sum'), + t.literal('avg'), + t.literal('median'), +]); +const fieldSortSchema = t.exact( + t.partial({ + missing: t.union([t.string, t.number, t.boolean]), + mode: sortModeSchema, + order: sortOrderSchema, + // nested and unmapped_type not implemented yet + }) +); +const sortContainerSchema = t.record(t.string, t.union([sortOrderSchema, fieldSortSchema])); +const sortCombinationsSchema = t.union([fieldSchema, sortContainerSchema]); +export const sortSchema = t.union([sortCombinationsSchema, t.array(sortCombinationsSchema)]); + +export const minDocCount = t.number; + +interface BucketAggsSchemas { + filter?: { + term?: { [x: string]: string | boolean | number }; + }; + histogram?: { + field?: string; + interval?: number; + min_doc_count?: number; + extended_bounds?: { + min: number; + max: number; + }; + hard_bounds?: { + min: number; + max: number; + }; + missing?: number; + keyed?: boolean; + order?: { + _count: string; + _key: string; + }; + }; + nested?: { + path: string; + }; + terms?: { + field?: string; + collect_mode?: string; + exclude?: string | string[]; + include?: string | string[]; + execution_hint?: string; + missing?: number | string; + min_doc_count?: number; + size?: number; + show_term_doc_count_error?: boolean; + order?: + | SortOrderSchema + | { [x: string]: SortOrderSchema } + | Array<{ [x: string]: SortOrderSchema }>; + }; + aggs?: { + [x: string]: BucketAggsSchemas; + }; +} + +/** + * Schemas for the Bucket aggregations. + * + * Currently supported: + * - filter + * - histogram + * - nested + * - terms + * + * Not implemented: + * - adjacency_matrix + * - auto_date_histogram + * - children + * - composite + * - date_histogram + * - date_range + * - diversified_sampler + * - filters + * - geo_distance + * - geohash_grid + * - geotile_grid + * - global + * - ip_range + * - missing + * - multi_terms + * - parent + * - range + * - rare_terms + * - reverse_nested + * - sampler + * - significant_terms + * - significant_text + * - variable_width_histogram + */ +export const BucketAggsSchemas: t.Type = t.recursion('BucketAggsSchemas', () => + t.exact( + t.partial({ + filter: t.exact( + t.partial({ + term: t.record(t.string, t.union([t.string, t.boolean, t.number])), + }) + ), + date_histogram: t.exact( + t.partial({ + field: t.string, + fixed_interval: t.string, + min_doc_count: t.number, + extended_bounds: t.type({ + min: t.string, + max: t.string, + }), + }) + ), + histogram: t.exact( + t.partial({ + field: t.string, + interval: t.number, + min_doc_count: t.number, + extended_bounds: t.exact( + t.type({ + min: t.number, + max: t.number, + }) + ), + hard_bounds: t.exact( + t.type({ + min: t.number, + max: t.number, + }) + ), + missing: t.number, + keyed: t.boolean, + order: t.exact( + t.type({ + _count: t.string, + _key: t.string, + }) + ), + }) + ), + nested: t.type({ + path: t.string, + }), + terms: t.exact( + t.partial({ + field: t.string, + collect_mode: t.string, + exclude: t.union([t.string, t.array(t.string)]), + include: t.union([t.string, t.array(t.string)]), + execution_hint: t.string, + missing: t.union([t.number, t.string]), + min_doc_count: t.number, + size: t.number, + show_term_doc_count_error: t.boolean, + order: t.union([ + sortOrderSchema, + t.record(t.string, sortOrderSchema), + t.array(t.record(t.string, sortOrderSchema)), + ]), + }) + ), + aggs: t.record(t.string, BucketAggsSchemas), + }) + ) +); + +/** + * Schemas for the metrics Aggregations + * + * Currently supported: + * - avg + * - cardinality + * - min + * - max + * - sum + * - top_hits + * - weighted_avg + * + * Not implemented: + * - boxplot + * - extended_stats + * - geo_bounds + * - geo_centroid + * - geo_line + * - matrix_stats + * - median_absolute_deviation + * - percentile_ranks + * - percentiles + * - rate + * - scripted_metric + * - stats + * - string_stats + * - t_test + * - value_count + */ +export const metricsAggsSchemas = t.partial({ + avg: t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + }), + cardinality: t.partial({ + field: t.string, + precision_threshold: t.number, + rehash: t.boolean, + missing: t.union([t.string, t.number, t.boolean]), + }), + min: t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + format: t.string, + }), + max: t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + format: t.string, + }), + sum: t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + }), + top_hits: t.partial({ + explain: t.boolean, + docvalue_fields: t.union([t.string, t.array(t.string)]), + stored_fields: t.union([t.string, t.array(t.string)]), + from: t.number, + size: t.number, + sort: sortSchema, + seq_no_primary_term: t.boolean, + version: t.boolean, + track_scores: t.boolean, + highlight: t.any, + _source: t.union([t.boolean, t.string, t.array(t.string)]), + }), + weighted_avg: t.partial({ + format: t.string, + value_type: t.string, + value: t.partial({ + field: t.string, + missing: t.number, + }), + weight: t.partial({ + field: t.string, + missing: t.number, + }), + }), +}); + +export type PutIndexTemplateRequest = estypes.IndicesPutIndexTemplateRequest & { + body?: { composed_of?: string[] }; +}; + export interface ClusterPutComponentTemplateBody { template: { settings: { diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md index 7c79e0a5e4c0f..75f3fd24cbc19 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md @@ -25,8 +25,11 @@ on alerts as data. - [buildEsQueryWithAuthz](alertsclient.md#buildesquerywithauthz) - [bulkUpdate](alertsclient.md#bulkupdate) - [ensureAllAuthorized](alertsclient.md#ensureallauthorized) +- [find](alertsclient.md#find) - [get](alertsclient.md#get) +- [getAlertStatusFieldUpdate](alertsclient.md#getalertstatusfieldupdate) - [getAuthorizedAlertsIndices](alertsclient.md#getauthorizedalertsindices) +- [getOutcome](alertsclient.md#getoutcome) - [mgetAlertsAuditOperate](alertsclient.md#mgetalertsauditoperate) - [queryAndAuditAllAlerts](alertsclient.md#queryandauditallalerts) - [singleSearchAfterAndAudit](alertsclient.md#singlesearchafterandaudit) @@ -46,7 +49,7 @@ on alerts as data. #### Defined in -[alerts_client.ts:93](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L93) +[alerts_client.ts:117](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L117) ## Properties @@ -56,7 +59,7 @@ on alerts as data. #### Defined in -[alerts_client.ts:90](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L90) +[alerts_client.ts:114](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L114) ___ @@ -66,7 +69,7 @@ ___ #### Defined in -[alerts_client.ts:91](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L91) +[alerts_client.ts:115](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L115) ___ @@ -76,7 +79,7 @@ ___ #### Defined in -[alerts_client.ts:92](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L92) +[alerts_client.ts:116](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L116) ___ @@ -86,7 +89,7 @@ ___ #### Defined in -[alerts_client.ts:89](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L89) +[alerts_client.ts:113](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L113) ___ @@ -96,7 +99,7 @@ ___ #### Defined in -[alerts_client.ts:93](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L93) +[alerts_client.ts:117](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L117) ## Methods @@ -108,10 +111,10 @@ ___ | Name | Type | | :------ | :------ | -| `query` | `undefined` \| ``null`` \| `string` | +| `query` | `undefined` \| ``null`` \| `string` \| `object` | | `id` | `undefined` \| ``null`` \| `string` | | `alertSpaceId` | `string` | -| `operation` | `Get` \| `Find` \| `Update` | +| `operation` | `Update` \| `Get` \| `Find` | | `config` | `EsQueryConfig` | #### Returns @@ -120,7 +123,7 @@ ___ #### Defined in -[alerts_client.ts:305](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L305) +[alerts_client.ts:367](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L367) ___ @@ -146,7 +149,7 @@ ___ #### Defined in -[alerts_client.ts:475](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L475) +[alerts_client.ts:570](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L570) ___ @@ -160,8 +163,8 @@ Accepts an array of ES documents and executes ensureAuthorized for the given ope | Name | Type | | :------ | :------ | -| `items` | { `_id`: `string` ; `_source?`: ``null`` \| { `kibana.alert.owner?`: ``null`` \| `string` ; `rule.id?`: ``null`` \| `string` } }[] | -| `operation` | `Get` \| `Find` \| `Update` | +| `items` | { `_id`: `string` ; `_source?`: ``null`` \| { `kibana.alert.rule.consumer?`: ``null`` \| `string` ; `kibana.alert.rule.rule_type_id?`: ``null`` \| `string` } }[] | +| `operation` | `Update` \| `Get` \| `Find` | #### Returns @@ -169,7 +172,39 @@ Accepts an array of ES documents and executes ensureAuthorized for the given ope #### Defined in -[alerts_client.ts:111](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L111) +[alerts_client.ts:152](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L152) + +___ + +### find + +▸ **find**(`__namedParameters`): `Promise`\>\>\> + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Params` | `Params`: `AlertTypeParams` = `never` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | `Object` | +| `__namedParameters._source?` | `string`[] | +| `__namedParameters.aggs?` | `object` | +| `__namedParameters.index` | `undefined` \| `string` | +| `__namedParameters.query?` | `object` | +| `__namedParameters.size?` | `number` | +| `__namedParameters.track_total_hits?` | `boolean` | + +#### Returns + +`Promise`\>\>\> + +#### Defined in + +[alerts_client.ts:628](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L628) ___ @@ -189,7 +224,28 @@ ___ #### Defined in -[alerts_client.ts:407](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L407) +[alerts_client.ts:491](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L491) + +___ + +### getAlertStatusFieldUpdate + +▸ `Private` **getAlertStatusFieldUpdate**(`source`, `status`): { `kibana.alert.workflow_status`: `undefined` ; `signal`: { `status`: `STATUS\_VALUES` } } \| { `kibana.alert.workflow_status`: `STATUS\_VALUES` ; `signal`: `undefined` } + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `source` | `undefined` \| `OutputOf`\> | +| `status` | `STATUS\_VALUES` | + +#### Returns + +{ `kibana.alert.workflow_status`: `undefined` ; `signal`: { `status`: `STATUS\_VALUES` } } \| { `kibana.alert.workflow_status`: `STATUS\_VALUES` ; `signal`: `undefined` } + +#### Defined in + +[alerts_client.ts:137](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L137) ___ @@ -209,7 +265,31 @@ ___ #### Defined in -[alerts_client.ts:533](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L533) +[alerts_client.ts:674](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L674) + +___ + +### getOutcome + +▸ `Private` **getOutcome**(`operation`): `Object` + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `operation` | `Update` \| `Get` \| `Find` | + +#### Returns + +`Object` + +| Name | Type | +| :------ | :------ | +| `outcome` | `EcsEventOutcome` | + +#### Defined in + +[alerts_client.ts:129](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L129) ___ @@ -226,7 +306,7 @@ When an update by ids is requested, do a multi-get, ensure authz and audit alert | `__namedParameters` | `Object` | | `__namedParameters.ids` | `string`[] | | `__namedParameters.indexName` | `string` | -| `__namedParameters.operation` | `Get` \| `Find` \| `Update` | +| `__namedParameters.operation` | `Update` \| `Get` \| `Find` | | `__namedParameters.status` | `STATUS\_VALUES` | #### Returns @@ -235,13 +315,13 @@ When an update by ids is requested, do a multi-get, ensure authz and audit alert #### Defined in -[alerts_client.ts:252](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L252) +[alerts_client.ts:308](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L308) ___ ### queryAndAuditAllAlerts -▸ `Private` **queryAndAuditAllAlerts**(`__namedParameters`): `Promise` +▸ `Private` **queryAndAuditAllAlerts**(`__namedParameters`): `Promise` executes a search after to find alerts with query (+ authz filter) @@ -251,16 +331,16 @@ executes a search after to find alerts with query (+ authz filter) | :------ | :------ | | `__namedParameters` | `Object` | | `__namedParameters.index` | `string` | -| `__namedParameters.operation` | `Get` \| `Find` \| `Update` | -| `__namedParameters.query` | `string` | +| `__namedParameters.operation` | `Update` \| `Get` \| `Find` | +| `__namedParameters.query` | `string` \| `object` | #### Returns -`Promise` +`Promise` #### Defined in -[alerts_client.ts:343](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L343) +[alerts_client.ts:423](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L423) ___ @@ -283,7 +363,7 @@ In the future we will add an "aggs" param #### Defined in -[alerts_client.ts:176](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L176) +[alerts_client.ts:220](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L220) ___ @@ -309,4 +389,4 @@ ___ #### Defined in -[alerts_client.ts:432](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L432) +[alerts_client.ts:520](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L520) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/bulkupdateoptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/bulkupdateoptions.md index 28c49c3519f6e..e27790aefbe2a 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/bulkupdateoptions.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/bulkupdateoptions.md @@ -25,7 +25,7 @@ #### Defined in -[alerts_client.ts:64](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L64) +[alerts_client.ts:84](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L84) ___ @@ -35,17 +35,17 @@ ___ #### Defined in -[alerts_client.ts:66](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L66) +[alerts_client.ts:86](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L86) ___ ### query -• **query**: `undefined` \| ``null`` \| `string` +• **query**: `undefined` \| ``null`` \| `string` \| `object` #### Defined in -[alerts_client.ts:67](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L67) +[alerts_client.ts:87](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L87) ___ @@ -55,4 +55,4 @@ ___ #### Defined in -[alerts_client.ts:65](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L65) +[alerts_client.ts:85](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L85) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md index c371719dbced3..a2e24106aa002 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md @@ -19,7 +19,7 @@ #### Defined in -[alerts_client.ts:52](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L52) +[alerts_client.ts:72](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L72) ___ @@ -29,7 +29,7 @@ ___ #### Defined in -[alerts_client.ts:51](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L51) +[alerts_client.ts:71](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L71) ___ @@ -39,7 +39,7 @@ ___ #### Defined in -[alerts_client.ts:53](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L53) +[alerts_client.ts:73](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L73) ___ @@ -49,4 +49,4 @@ ___ #### Defined in -[alerts_client.ts:50](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L50) +[alerts_client.ts:70](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L70) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md index f05a061b279d9..b868123345b4a 100644 --- a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md @@ -25,7 +25,7 @@ #### Defined in -[alerts_client.ts:59](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) +[alerts_client.ts:79](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L79) ___ @@ -35,7 +35,7 @@ ___ #### Defined in -[alerts_client.ts:57](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L57) +[alerts_client.ts:77](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L77) ___ @@ -45,7 +45,7 @@ ___ #### Defined in -[alerts_client.ts:60](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L60) +[alerts_client.ts:80](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L80) ___ @@ -55,4 +55,4 @@ ___ #### Defined in -[alerts_client.ts:58](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L58) +[alerts_client.ts:78](https://github.com/elastic/kibana/blob/42f5a948210/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L78) diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index 360ea18df9ca1..a750c4a91072a 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -2,15 +2,8 @@ "id": "ruleRegistry", "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "ruleRegistry" - ], - "requiredPlugins": [ - "alerting", - "data", - "triggersActionsUi" - ], + "configPath": ["xpack", "ruleRegistry"], + "requiredPlugins": ["alerting", "data", "triggersActionsUi"], "optionalPlugins": ["security"], "server": true } diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts index ee81a39052522..d2e841a79cb31 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts @@ -17,6 +17,7 @@ const createAlertsClientMock = () => { update: jest.fn(), getAuthorizedAlertsIndices: jest.fn(), bulkUpdate: jest.fn(), + find: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index e78f5f6d51cd2..d2a8b914d10b6 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -56,7 +56,7 @@ type AlertType = { _index: string; _id: string } & NonNullableProps< typeof ALERT_RULE_TYPE_ID | typeof ALERT_RULE_CONSUMER | typeof SPACE_IDS >; -const isValidAlert = (source?: estypes.SearchHit): source is AlertType => { +const isValidAlert = (source?: estypes.SearchHit): source is AlertType => { return ( (source?._source?.[ALERT_RULE_TYPE_ID] != null && source?._source?.[ALERT_RULE_CONSUMER] != null && @@ -93,11 +93,15 @@ interface GetAlertParams { } interface SingleSearchAfterAndAudit { - id: string | null | undefined; - query: object | string | null | undefined; + id?: string | null | undefined; + query?: string | object | undefined; + aggs?: object | undefined; index?: string; + _source?: string[] | undefined; + track_total_hits?: boolean | undefined; + size?: number | undefined; operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get; - lastSortIds: Array | undefined; + lastSortIds?: Array | undefined; } /** @@ -216,6 +220,10 @@ export class AlertsClient { private async singleSearchAfterAndAudit({ id, query, + aggs, + _source, + track_total_hits: trackTotalHits, + size, index, operation, lastSortIds = [], @@ -233,6 +241,10 @@ export class AlertsClient { let queryBody = { fields: [ALERT_RULE_TYPE_ID, ALERT_RULE_CONSUMER, ALERT_WORKFLOW_STATUS, SPACE_IDS], query: await this.buildEsQueryWithAuthz(query, id, alertSpaceId, operation, config), + aggs, + _source, + track_total_hits: trackTotalHits, + size, sort: [ { '@timestamp': { @@ -265,17 +277,19 @@ export class AlertsClient { throw Boom.badData(errorMessage); } - await this.ensureAllAuthorized(result.body.hits.hits, operation); - - result?.body.hits.hits.map((item) => - this.auditLogger?.log( - alertAuditEvent({ - action: operationAlertAuditActionMap[operation], - id: item._id, - ...this.getOutcome(operation), - }) - ) - ); + if (result?.body?.hits?.hits != null && result?.body.hits.hits.length > 0) { + await this.ensureAllAuthorized(result.body.hits.hits, operation); + + result?.body.hits.hits.map((item) => + this.auditLogger?.log( + alertAuditEvent({ + action: operationAlertAuditActionMap[operation], + id: item._id, + ...this.getOutcome(operation), + }) + ) + ); + } return result.body; } catch (error) { @@ -474,10 +488,8 @@ export class AlertsClient { // first search for the alert by id, then use the alert info to check if user has access to it const alert = await this.singleSearchAfterAndAudit({ id, - query: undefined, index, operation: ReadOperations.Get, - lastSortIds: undefined, }); if (alert == null || alert.hits.hits.length === 0) { @@ -503,10 +515,8 @@ export class AlertsClient { try { const alert = await this.singleSearchAfterAndAudit({ id, - query: null, index, operation: WriteOperations.Update, - lastSortIds: undefined, }); if (alert == null || alert.hits.hits.length === 0) { @@ -565,7 +575,7 @@ export class AlertsClient { }); if (!fetchAndAuditResponse?.auditedAlerts) { - throw Boom.unauthorized('Failed to audit alerts'); + throw Boom.forbidden('Failed to audit alerts'); } // executes updateByQuery with query + authorization filter @@ -598,6 +608,46 @@ export class AlertsClient { } } + public async find({ + query, + aggs, + _source, + track_total_hits: trackTotalHits, + size, + index, + }: { + query?: object | undefined; + aggs?: object | undefined; + index: string | undefined; + track_total_hits?: boolean | undefined; + _source?: string[] | undefined; + size?: number | undefined; + }) { + try { + // first search for the alert by id, then use the alert info to check if user has access to it + const alertsSearchResponse = await this.singleSearchAfterAndAudit({ + query, + aggs, + _source, + track_total_hits: trackTotalHits, + size, + index, + operation: ReadOperations.Find, + }); + + if (alertsSearchResponse == null) { + const errorMessage = `Unable to retrieve alert details for alert with query and operation ${ReadOperations.Find}`; + this.logger.error(errorMessage); + throw Boom.notFound(errorMessage); + } + + return alertsSearchResponse; + } catch (error) { + this.logger.error(`find threw an error: ${error}`); + throw error; + } + } + public async getAuthorizedAlertsIndices(featureIds: string[]): Promise { try { const augmentedRuleTypes = await this.authorization.getAugmentedRuleTypesWithAuthorization( @@ -616,7 +666,11 @@ export class AlertsClient { const toReturn = Array.from(authorizedFeatures).flatMap((feature) => { if (isValidFeatureId(feature)) { - return mapConsumerToIndexName[feature]; + if (feature === 'siem') { + return `${mapConsumerToIndexName[feature]}-${this.spaceId}`; + } else { + return `${mapConsumerToIndexName[feature]}`; + } } return []; }); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts index a6d42853531d7..11066ffddfadd 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts @@ -41,6 +41,12 @@ beforeEach(() => { alertingAuthMock.getAuthorizationFilter.mockImplementation(async () => Promise.resolve({ filter: [] }) ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); alertingAuthMock.ensureAuthorized.mockImplementation( // @ts-expect-error async ({ diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts new file mode 100644 index 0000000000000..1e6601c7b0862 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts @@ -0,0 +1,422 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, + SPACE_IDS, + ALERT_WORKFLOW_STATUS, +} from '@kbn/rule-data-utils'; +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; +import { AuditLogger } from '../../../../security/server'; +import { AlertingAuthorizationEntity } from '../../../../alerting/server'; + +const alertingAuthMock = alertingAuthorizationMock.create(); +const esClientMock = elasticsearchClientMock.createElasticsearchClient(); +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + authorization: alertingAuthMock, + esClient: esClientMock, + auditLogger, +}; + +const DEFAULT_SPACE = 'test_default_space_id'; + +beforeEach(() => { + jest.resetAllMocks(); + alertingAuthMock.getSpaceId.mockImplementation(() => DEFAULT_SPACE); + // @ts-expect-error + alertingAuthMock.getAuthorizationFilter.mockImplementation(async () => + Promise.resolve({ filter: [] }) + ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); + + alertingAuthMock.ensureAuthorized.mockImplementation( + // @ts-expect-error + async ({ + ruleTypeId, + consumer, + operation, + entity, + }: { + ruleTypeId: string; + consumer: string; + operation: string; + entity: typeof AlertingAuthorizationEntity.Alert; + }) => { + if (ruleTypeId === 'apm.error_rate' && consumer === 'apm') { + return Promise.resolve(); + } + return Promise.reject(new Error(`Unauthorized for ${ruleTypeId} and ${consumer}`)); + } + ); +}); + +describe('find()', () => { + test('calls ES client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + message: 'hello world 1', + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['test_default_space_id'], + }, + }, + ], + }, + }, + }) + ); + const result = await alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "_shards": Object { + "failed": 0, + "skipped": 0, + "successful": 1, + "total": 1, + }, + "hits": Object { + "hits": Array [ + Object { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_primary_term": 2, + "_seq_no": 362, + "_source": Object { + "kibana.alert.rule.consumer": "apm", + "kibana.alert.rule.rule_type_id": "apm.error_rate", + "kibana.alert.workflow_status": "open", + "kibana.space_ids": Array [ + "test_default_space_id", + ], + "message": "hello world 1", + }, + "_type": "alert", + "_version": 1, + "found": true, + }, + ], + "max_score": 999, + "total": 1, + }, + "timed_out": false, + "took": 5, + } + `); + expect(esClientMock.search).toHaveBeenCalledTimes(1); + expect(esClientMock.search.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "_source": undefined, + "aggs": undefined, + "fields": Array [ + "kibana.alert.rule.rule_type_id", + "kibana.alert.rule.consumer", + "kibana.alert.workflow_status", + "kibana.space_ids", + ], + "query": Object { + "bool": Object { + "filter": Array [ + Object {}, + Object { + "term": Object { + "kibana.space_ids": "test_default_space_id", + }, + }, + ], + "must": Array [ + Object { + "match": Object { + "kibana.alert.workflow_status": "open", + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + }, + "size": undefined, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "asc", + "unmapped_type": "date", + }, + }, + ], + "track_total_hits": undefined, + }, + "ignore_unavailable": true, + "index": ".alerts-observability-apm", + "seq_no_primary_term": true, + }, + ] + `); + }); + + test('logs successful event in audit logger', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + message: 'hello world 1', + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['test_default_space_id'], + }, + }, + ], + }, + }, + }) + ); + await alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }); + + expect(auditLogger.log).toHaveBeenCalledWith({ + error: undefined, + event: { action: 'alert_find', category: ['database'], outcome: 'success', type: ['access'] }, + message: 'User has accessed alert [id=NoxgpHkBqbdrfX07MqXV]', + }); + }); + + test('audit error access if user is unauthorized for given alert', async () => { + const indexName = '.alerts-observability-apm'; + const fakeAlertId = 'myfakeid1'; + // fakeRuleTypeId will cause authz to fail + const fakeRuleTypeId = 'fake.rule'; + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _id: fakeAlertId, + _index: indexName, + _source: { + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: [DEFAULT_SPACE], + }, + }, + ], + }, + }, + }) + ); + + await expect( + alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find + Error: Error: Unauthorized for fake.rule and apm" + `); + + expect(auditLogger.log).toHaveBeenNthCalledWith(1, { + message: `Failed attempt to access alert [id=${fakeAlertId}]`, + event: { + action: 'alert_find', + category: ['database'], + outcome: 'failure', + type: ['access'], + }, + error: { + code: 'Error', + message: 'Unauthorized for fake.rule and apm', + }, + }); + }); + + test(`throws an error if ES client get fails`, async () => { + const error = new Error('something went wrong'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockRejectedValue(error); + + await expect( + alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find + Error: Error: something went wrong" + `); + }); + + describe('authorization', () => { + beforeEach(() => { + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + message: 'hello world 1', + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['test_default_space_id'], + }, + }, + ], + }, + }, + }) + ); + }); + + test('returns alert if user is authorized to read alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.find({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + index: '.alerts-observability-apm', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "_shards": Object { + "failed": 0, + "skipped": 0, + "successful": 1, + "total": 1, + }, + "hits": Object { + "hits": Array [ + Object { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_primary_term": 2, + "_seq_no": 362, + "_source": Object { + "kibana.alert.rule.consumer": "apm", + "kibana.alert.rule.rule_type_id": "apm.error_rate", + "kibana.alert.workflow_status": "open", + "kibana.space_ids": Array [ + "test_default_space_id", + ], + "message": "hello world 1", + }, + "_type": "alert", + "_version": 1, + "found": true, + }, + ], + "max_score": 999, + "total": 1, + }, + "timed_out": false, + "took": 5, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts index 1a0628bf6e9a8..2f299142166d6 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -42,6 +42,13 @@ beforeEach(() => { Promise.resolve({ filter: [] }) ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); + alertingAuthMock.ensureAuthorized.mockImplementation( // @ts-expect-error async ({ @@ -119,6 +126,8 @@ describe('get()', () => { Array [ Object { "body": Object { + "_source": undefined, + "aggs": undefined, "fields": Array [ "kibana.alert.rule.rule_type_id", "kibana.alert.rule.consumer", @@ -152,6 +161,7 @@ describe('get()', () => { "should": Array [], }, }, + "size": undefined, "sort": Array [ Object { "@timestamp": Object { @@ -160,6 +170,7 @@ describe('get()', () => { }, }, ], + "track_total_hits": undefined, }, "ignore_unavailable": true, "index": ".alerts-observability-apm", @@ -258,8 +269,8 @@ describe('get()', () => { }) ); - await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability-apm' })).rejects - .toThrowErrorMatchingInlineSnapshot(` + await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability-apm.alerts' })) + .rejects.toThrowErrorMatchingInlineSnapshot(` "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation get Error: Error: Unauthorized for fake.rule and apm" `); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts index 4e084c2c028b1..90ca2da06ccdf 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -42,6 +42,13 @@ beforeEach(() => { Promise.resolve({ filter: [] }) ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); + alertingAuthMock.ensureAuthorized.mockImplementation( // @ts-expect-error async ({ @@ -267,7 +274,7 @@ describe('update()', () => { index: '.alerts-observability-apm', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"null\\" and operation update + "Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation update Error: Error: Unauthorized for fake.rule and apm" `); @@ -299,7 +306,7 @@ describe('update()', () => { index: '.alerts-observability-apm', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"null\\" and operation update + "Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation update Error: Error: something went wrong on update" `); }); diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts index 6d47882ca86c4..0555b3320ad91 100644 --- a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts @@ -11,7 +11,6 @@ import { RacRequestHandlerContext } from '../../types'; const createMockClients = () => ({ rac: alertsClientMock.create(), - clusterClient: elasticsearchServiceMock.createLegacyScopedClusterClient(), newClusterClient: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: savedObjectsClientMock.create(), }); @@ -27,7 +26,6 @@ const createRequestContextMock = ( elasticsearch: { ...coreContext.elasticsearch, client: clients.newClusterClient, - legacy: { ...coreContext.elasticsearch.legacy, client: clients.clusterClient }, }, savedObjects: { client: clients.savedObjectsClient }, }, diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts index 228fcf491994f..d591e01c9fff6 100644 --- a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts @@ -8,6 +8,13 @@ import { BASE_RAC_ALERTS_API_PATH } from '../../../common/constants'; import { requestMock } from './server'; +export const getReadIndexRequest = () => + requestMock.create({ + method: 'get', + path: `${BASE_RAC_ALERTS_API_PATH}/index`, + query: { features: 'siem' }, + }); + export const getReadRequest = () => requestMock.create({ method: 'get', diff --git a/x-pack/plugins/rule_registry/server/routes/find.ts b/x-pack/plugins/rule_registry/server/routes/find.ts new file mode 100644 index 0000000000000..8fb3c116e171c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/find.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { buildRouteValidation } from './utils/route_validation'; +import { BucketAggsSchemas } from '../../common/types'; + +export const findAlertsByQueryRoute = (router: IRouter) => { + router.post( + { + path: `${BASE_RAC_ALERTS_API_PATH}/find`, + validate: { + body: buildRouteValidation( + t.exact( + t.partial({ + index: t.string, + query: t.object, + aggs: t.union([t.record(t.string, BucketAggsSchemas), t.undefined]), + size: t.union([PositiveInteger, t.undefined]), + track_total_hits: t.union([t.boolean, t.undefined]), + _source: t.union([t.array(t.string), t.undefined]), + }) + ) + ), + }, + options: { + tags: ['access:rac'], + }, + }, + async (context, request, response) => { + try { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { query, aggs, _source, track_total_hits, size, index } = request.body; + + const alertsClient = await context.rac.getAlertsClient(); + + const alerts = await alertsClient.find({ + query, + aggs, + _source, + track_total_hits, + size, + index, + }); + if (alerts == null) { + return response.notFound({ + body: { message: `alerts with query and index ${index} not found` }, + }); + } + return response.ok({ + body: alerts, + }); + } catch (exc) { + const err = transformError(exc); + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.customError({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + success: false, + }, + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_index.test.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_index.test.ts new file mode 100644 index 0000000000000..b8ef01847d8ea --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_index.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { getAlertsIndexRoute } from './get_alert_index'; +import { requestContextMock } from './__mocks__/request_context'; +import { getReadIndexRequest } from './__mocks__/request_responses'; +import { requestMock, serverMock } from './__mocks__/server'; + +describe('getAlertsIndexRoute', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(async () => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + clients.rac.getAuthorizedAlertsIndices.mockResolvedValue(['alerts-security.alerts']); + + getAlertsIndexRoute(server.router); + }); + + test('returns 200 when querying for index', async () => { + const response = await server.inject(getReadIndexRequest(), context); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ index_name: ['alerts-security.alerts'] }); + }); + + describe('request validation', () => { + test('rejects invalid query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'get', + path: `${BASE_RAC_ALERTS_API_PATH}/index`, + query: { features: 4 }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"4\\" supplied to \\"features\\"'"` + ); + }); + + test('rejects unknown query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'get', + path: `${BASE_RAC_ALERTS_API_PATH}/index`, + query: { boop: 'siem' }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'invalid keys \\"boop\\"'"` + ); + }); + }); + + test('returns error status if rac client "getAuthorizedAlertsIndices" fails', async () => { + clients.rac.getAuthorizedAlertsIndices.mockRejectedValue(new Error('Unable to get index')); + const response = await server.inject(getReadIndexRequest(), context); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + attributes: { success: false }, + message: 'Unable to get index', + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts index f3b0b9181c60f..758057c21c43e 100644 --- a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts @@ -6,9 +6,11 @@ */ import { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; import { transformError } from '@kbn/securitysolution-es-utils'; import { validFeatureIds } from '@kbn/rule-data-utils'; +import { buildRouteValidation } from './utils/route_validation'; import { RacRequestHandlerContext } from '../types'; import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; @@ -17,7 +19,15 @@ export const getAlertsIndexRoute = (router: IRouter) = router.get( { path: `${BASE_RAC_ALERTS_API_PATH}/index`, - validate: false, + validate: { + query: buildRouteValidation( + t.exact( + t.partial({ + features: t.string, + }) + ) + ), + }, options: { tags: ['access:rac'], }, @@ -25,7 +35,10 @@ export const getAlertsIndexRoute = (router: IRouter) = async (context, request, response) => { try { const alertsClient = await context.rac.getAlertsClient(); - const indexName = await alertsClient.getAuthorizedAlertsIndices(validFeatureIds); + const { features } = request.query; + const indexName = await alertsClient.getAuthorizedAlertsIndices( + features?.split(',') ?? validFeatureIds + ); return response.ok({ body: { index_name: indexName }, }); @@ -38,15 +51,15 @@ export const getAlertsIndexRoute = (router: IRouter) = ...contentType, }; - return response.custom({ + return response.customError({ headers: defaultedHeaders, statusCode: err.statusCode, - body: Buffer.from( - JSON.stringify({ - message: err.message, - status_code: err.statusCode, - }) - ), + body: { + message: err.message, + attributes: { + success: false, + }, + }, }); } } diff --git a/x-pack/plugins/rule_registry/server/routes/index.ts b/x-pack/plugins/rule_registry/server/routes/index.ts index 0b900f26e56e6..4de121c7b9b5e 100644 --- a/x-pack/plugins/rule_registry/server/routes/index.ts +++ b/x-pack/plugins/rule_registry/server/routes/index.ts @@ -11,10 +11,12 @@ import { getAlertByIdRoute } from './get_alert_by_id'; import { updateAlertByIdRoute } from './update_alert_by_id'; import { getAlertsIndexRoute } from './get_alert_index'; import { bulkUpdateAlertsRoute } from './bulk_update_alerts'; +import { findAlertsByQueryRoute } from './find'; export function defineRoutes(router: IRouter) { getAlertByIdRoute(router); updateAlertByIdRoute(router); getAlertsIndexRoute(router); bulkUpdateAlertsRoute(router); + findAlertsByQueryRoute(router); } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 7ad5926d53d08..e53e1db5391e7 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -241,59 +241,72 @@ export class ResourceInstaller { logger.debug(`Creating write target for ${primaryNamespacedAlias}`); - const { body: indicesExist } = await clusterClient.indices.exists({ - index: primaryNamespacedPattern, - allow_no_indices: false, - }); - - if (!indicesExist) { - await this.installNamespacedIndexTemplate(indexInfo, namespace); - - try { - await clusterClient.indices.create({ - index: initialIndexName, - body: { - aliases: { - [primaryNamespacedAlias]: { - is_write_index: true, - }, - }, - }, - }); - } catch (err) { - // If the index already exists and it's the write index for the alias, - // something else created it so suppress the error. If it's not the write - // index, that's bad, throw an error. - if (err?.meta?.body?.error?.type === 'resource_already_exists_exception') { - const { body: existingIndices } = await clusterClient.indices.get({ - index: initialIndexName, - }); - if ( - !existingIndices[initialIndexName]?.aliases?.[primaryNamespacedAlias]?.is_write_index - ) { - throw Error( - `Attempted to create index: ${initialIndexName} as the write index for alias: ${primaryNamespacedAlias}, but the index already exists and is not the write index for the alias` - ); - } - } else { - throw err; - } - } - } else { - // If we find indices matching the pattern, then we expect one of them to be the write index for the alias. - // Throw an error if none of them are the write index. - const { body: aliasesResponse } = await clusterClient.indices.getAlias({ + try { + // When a new namespace is created we expect getAlias to return a 404 error, + // we'll catch it below and continue on. A non-404 error is a real problem so we throw. + + // It's critical that we specify *both* the index pattern and alias in this request. The alias prevents the + // request from finding other namespaces that could match the -* part of the index pattern + // (see https://github.com/elastic/kibana/issues/107704). The index pattern prevents the request from + // finding legacy .siem-signals indices that we add the alias to for backwards compatibility reasons. Together, + // the index pattern and alias should ensure that we retrieve only the "new" backing indices for this + // particular alias. + const { body: aliases } = await clusterClient.indices.getAlias({ index: primaryNamespacedPattern, + name: primaryNamespacedAlias, }); + + // If we find backing indices for the alias here, we shouldn't be making a new concrete index - + // either one of the indices is the write index so we return early because we don't need a new write target, + // or none of them are the write index so we'll throw an error because one of the existing indices should have + // been the write target if ( - !Object.entries(aliasesResponse).some( - ([_, aliasesObject]) => aliasesObject.aliases[primaryNamespacedAlias]?.is_write_index + Object.values(aliases).some( + (aliasesObject) => aliasesObject.aliases[primaryNamespacedAlias].is_write_index ) ) { - throw Error( + return; + } else { + throw new Error( `Indices matching pattern ${primaryNamespacedPattern} exist but none are set as the write index for alias ${primaryNamespacedAlias}` ); } + } catch (err) { + // 404 is expected if the alerts-as-data index hasn't been created yet + if (err.statusCode !== 404) { + throw err; + } + } + + await this.installNamespacedIndexTemplate(indexInfo, namespace); + + try { + await clusterClient.indices.create({ + index: initialIndexName, + body: { + aliases: { + [primaryNamespacedAlias]: { + is_write_index: true, + }, + }, + }, + }); + } catch (err) { + // If the index already exists and it's the write index for the alias, + // something else created it so suppress the error. If it's not the write + // index, that's bad, throw an error. + if (err?.meta?.body?.error?.type === 'resource_already_exists_exception') { + const { body: existingIndices } = await clusterClient.indices.get({ + index: initialIndexName, + }); + if (!existingIndices[initialIndexName]?.aliases?.[primaryNamespacedAlias]?.is_write_index) { + throw Error( + `Attempted to create index: ${initialIndexName} as the write index for alias: ${primaryNamespacedAlias}, but the index already exists and is not the write index for the alias` + ); + } + } else { + throw err; + } } } diff --git a/x-pack/plugins/rule_registry/server/scripts/find_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/find_observability_alert.sh new file mode 100755 index 0000000000000..4c4ee5f75836c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/find_observability_alert.sh @@ -0,0 +1,29 @@ +#!/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 +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e + +IDS=${1:-[\"Do4JnHoBqkRSppNZ6vre\"]} +STATUS=${2} + +echo $IDS +echo "'"$STATUS"'" + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./update_observability_alert.sh [\"my-alert-id\",\"another-alert-id\"] +# curl -s -k \ +curl -v \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u observer:changeme \ + -X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts/find \ +-d "{\"query\": { \"match\": { \"kibana.alert.status\": \"open\" }}, \"index\":\".alerts-observability-apm\"}" | jq . \ No newline at end of file diff --git a/x-pack/plugins/runtime_fields/kibana.json b/x-pack/plugins/runtime_fields/kibana.json index 65932c723c474..ef5514a01b3cf 100644 --- a/x-pack/plugins/runtime_fields/kibana.json +++ b/x-pack/plugins/runtime_fields/kibana.json @@ -3,13 +3,12 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": [ - ], - "optionalPlugins": [ - ], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "requiredPlugins": [], + "optionalPlugins": [], "configPath": ["xpack", "runtime_fields"], - "requiredBundles": [ - "kibanaReact", - "esUiShared" - ] + "requiredBundles": ["kibanaReact", "esUiShared"] } diff --git a/x-pack/plugins/searchprofiler/kibana.json b/x-pack/plugins/searchprofiler/kibana.json index 6c94701c0ec09..864e3880ae200 100644 --- a/x-pack/plugins/searchprofiler/kibana.json +++ b/x-pack/plugins/searchprofiler/kibana.json @@ -5,6 +5,10 @@ "configPath": ["xpack", "searchprofiler"], "server": true, "ui": true, + "owner": { + "name": "Stack Management", + "githubTeam": "kibana-stack-management" + }, "requiredPlugins": ["devTools", "home", "licensing"], "requiredBundles": ["esUiShared"] } diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index d9452a537826d..548716880478b 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -251,6 +251,9 @@ export const DETECTION_ENGINE_SIGNALS_MIGRATION_URL = `${DETECTION_ENGINE_SIGNAL export const DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/migration_status`; export const DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL = `${DETECTION_ENGINE_SIGNALS_URL}/finalize_migration`; +export const ALERTS_AS_DATA_URL = '/internal/rac/alerts'; +export const ALERTS_AS_DATA_FIND_URL = `${ALERTS_AS_DATA_URL}/find`; + /** * Common naming convention for an unauthenticated user */ @@ -290,11 +293,6 @@ if (ENABLE_CASE_CONNECTOR) { export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions'; export const NOTIFICATION_THROTTLE_RULE = 'rule'; -/** - * Histograms for fields named in this list should be displayed with an - * "All others" bucket, to count events that don't specify a value for - * the field being counted - */ export const showAllOthersBucket: string[] = [ 'destination.ip', 'event.action', 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 c869a12faf360..ab89ab31acc02 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 @@ -170,7 +170,12 @@ export type RuleNameOverride = t.TypeOf; export const ruleNameOverrideOrUndefined = t.union([rule_name_override, t.undefined]); export type RuleNameOverrideOrUndefined = t.TypeOf; -export const status = t.keyof({ open: null, closed: null, 'in-progress': null }); +export const status = t.keyof({ + open: null, + closed: null, + acknowledged: null, + 'in-progress': null, // TODO: Remove after `acknowledged` migrations +}); export type Status = t.TypeOf; export enum RuleExecutionStatus { diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 8bd8edb9424b4..51211705db573 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -15,6 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ metricsEntitiesEnabled: false, ruleRegistryEnabled: false, tGridEnabled: true, + tGridEventRenderedViewEnabled: true, trustedAppsByPolicyEnabled: false, excludePoliciesInFilterEnabled: false, uebaEnabled: false, diff --git a/x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts b/x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts index 257a6f0c30981..772c16fc9cb99 100644 --- a/x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts +++ b/x-pack/plugins/security_solution/common/machine_learning/empty_ml_capabilities.ts @@ -18,6 +18,7 @@ export const emptyMlCapabilities: MlCapabilitiesResponse = { canDeleteJob: false, canOpenJob: false, canCloseJob: false, + canResetJob: false, canForecastJob: false, canGetDatafeeds: false, canStartStopDatafeed: false, diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 6a3d812b1bf5b..cdd9b35a7fa30 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -460,6 +460,17 @@ export enum TimelineTabs { eql = 'eql', } +/** + * Used for scrolling top inside a tab. Especially when swiching tabs. + */ +export interface ScrollToTopEvent { + /** + * Timestamp of the moment when the event happened. + * The timestamp might be necessary for the scenario where the event could happen multiple times. + */ + timestamp: number; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any type EmptyObject = Record; diff --git a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts index 26f2385d94f1f..3263eb9d70b66 100644 --- a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts @@ -58,7 +58,7 @@ describe('Alert details with unmapped fields', () => { it('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { - row: 88, + row: 90, field: 'unmapped', text: 'This is the unmapped field', }; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts similarity index 82% rename from x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts index d82bfe00659f3..f933c5a4ed0a2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts @@ -13,8 +13,8 @@ import { waitForAlertsPanelToBeLoaded, waitForAlerts, waitForAlertsToBeLoaded, - markInProgressFirstAlert, - goToInProgressAlerts, + markAcknowledgedFirstAlert, + goToAcknowledgedAlerts, waitForAlertsIndexToBeCreated, goToOpenedAlerts, } from '../../tasks/alerts'; @@ -26,7 +26,7 @@ import { refreshPage } from '../../tasks/security_header'; import { ALERTS_URL } from '../../urls/navigation'; -describe('Marking alerts as in-progress', () => { +describe('Marking alerts as acknowledged', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPage(ALERTS_URL); @@ -37,30 +37,30 @@ describe('Marking alerts as in-progress', () => { waitForAlertsToPopulate(500); }); - it('Mark one alert in progress when more than one open alerts are selected', () => { + it('Mark one alert as acknowledged when more than one open alerts are selected', () => { cy.get(ALERTS_COUNT) .invoke('text') .then((alertNumberString) => { const numberOfAlerts = alertNumberString.split(' ')[0]; - const numberOfAlertsToBeMarkedInProgress = 1; + const numberOfAlertsToBeMarkedAcknowledged = 1; const numberOfAlertsToBeSelected = 3; cy.get(TAKE_ACTION_POPOVER_BTN).should('not.exist'); selectNumberOfAlerts(numberOfAlertsToBeSelected); cy.get(TAKE_ACTION_POPOVER_BTN).should('exist'); - markInProgressFirstAlert(); + markAcknowledgedFirstAlert(); refreshPage(); waitForAlertsToBeLoaded(); goToOpenedAlerts(); - const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeMarkedInProgress; + const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeMarkedAcknowledged; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); - goToInProgressAlerts(); + goToAcknowledgedAlerts(); waitForAlerts(); - cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlertsToBeMarkedInProgress} alert`); + cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlertsToBeMarkedAcknowledged} alert`); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index 221fd4ae3266e..8ce3de6e5d7ac 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -37,8 +37,7 @@ import { import { ALERTS_URL } from '../../urls/navigation'; import { addsFieldsToTimeline } from '../../tasks/rule_details'; -// TODO: Doesn't look like the roll over is happening for these tests. 'indicator' is still referenced in the fields browser -describe.skip('CTI Enrichment', () => { +describe('CTI Enrichment', () => { before(() => { cleanKibana(); esArchiverLoad('threat_indicator'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index 016e8b3f8f0a1..5e77366618d08 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -8,7 +8,7 @@ import { formatMitreAttackDescription } from '../../helpers/rules'; import { getEqlRule, getEqlSequenceRule, getIndexPatterns } from '../../objects/rule'; -import { ALERT_GRID_CELL, NUMBER_OF_ALERTS } from '../../screens/alerts'; +import { ALERT_DATA_GRID, NUMBER_OF_ALERTS } from '../../screens/alerts'; import { CUSTOM_RULES_BTN, RISK_SCORE, @@ -161,11 +161,13 @@ describe('Detection rules, EQL', () => { waitForAlertsToPopulate(); cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); - // EuiDataGrid doesn't seem to have a way to apply data-test-subj to the individual cells - // Also, text detailing the row and column shows up in this search so switched 'have.text' to 'contains' - cy.get(ALERT_GRID_CELL).eq(3).contains(this.rule.name); - cy.get(ALERT_GRID_CELL).eq(4).contains(this.rule.severity.toLowerCase()); - cy.get(ALERT_GRID_CELL).eq(5).contains(this.rule.riskScore); + cy.get(ALERT_DATA_GRID) + .invoke('text') + .then((text) => { + expect(text).contains(this.rule.name); + expect(text).contains(this.rule.severity.toLowerCase()); + expect(text).contains(this.rule.riskScore); + }); }); }); @@ -213,10 +215,13 @@ describe('Detection rules, sequence EQL', () => { waitForAlertsToPopulate(); cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfSequenceAlerts); - // EuiDataGrid doesn't seem to have a way to apply data-test-subj to the individual cells - // Also, text detailing the row and column shows up in this search so switched 'have.text' to 'contains' - cy.get(ALERT_GRID_CELL).eq(3).contains(this.rule.name); - cy.get(ALERT_GRID_CELL).eq(4).contains(this.rule.severity.toLowerCase()); - cy.get(ALERT_GRID_CELL).eq(5).contains(this.rule.riskScore); + cy.get(ALERT_DATA_GRID) + .invoke('text') + .then((text) => { + cy.log('ALERT_DATA_GRID', text); + expect(text).contains(this.rule.name); + expect(text).contains(this.rule.severity.toLowerCase()); + expect(text).contains(this.rule.riskScore); + }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 6b8afc5da4949..f8b3b426580b2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -116,7 +116,7 @@ describe('indicator match', () => { const expectedTags = getNewThreatIndicatorRule().tags.join(''); const expectedMitre = formatMitreAttackDescription(getNewThreatIndicatorRule().mitre); const expectedNumberOfRules = 1; - const expectedNumberOfAlerts = 1; + const expectedNumberOfAlerts = '1 alert'; before(() => { cleanKibana(); @@ -139,8 +139,7 @@ describe('indicator match', () => { getIndicatorIndex().should('have.text', getIndexPatterns().join('')); }); - // TODO: Need to fix - it.skip('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => { + it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => { getDefineContinueButton().click(); getIndexPatternInvalidationText().should('not.exist'); }); @@ -154,8 +153,7 @@ describe('indicator match', () => { }); describe('Indicator index patterns', () => { - // TODO: Need to fix - it.skip('Contains a predefined index pattern', () => { + it('Contains a predefined index pattern', () => { getIndicatorIndicatorIndex().should('have.text', getThreatIndexPatterns().join('')); }); @@ -392,8 +390,7 @@ describe('indicator match', () => { loginAndWaitForPageWithoutDateRange(ALERTS_URL); }); - // TODO: Need to fix - it.skip('Creates and activates a new Indicator Match rule', () => { + it('Creates and activates a new Indicator Match rule', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertsDetectionRules(); @@ -491,8 +488,7 @@ describe('indicator match', () => { .should('have.text', getNewThreatIndicatorRule().riskScore); }); - // TODO: Need to fix - it.skip('Investigate alert in timeline', () => { + it('Investigate alert in timeline', () => { const accessibilityText = `Press enter for options, or press space to begin dragging.`; loadPrepackagedTimelineTemplates(); diff --git a/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts index 15982f1674351..16278c919a046 100644 --- a/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts @@ -51,7 +51,7 @@ import { } from '../../screens/kibana_navigation'; import { cleanKibana } from '../../tasks/common'; -describe('top-level navigation common to all pages in the Security app', () => { +describe.skip('top-level navigation common to all pages in the Security app', () => { before(() => { cleanKibana(); loginAndWaitForPage(TIMELINES_URL); @@ -111,7 +111,7 @@ describe('top-level navigation common to all pages in the Security app', () => { }); }); -describe('Kibana navigation to all pages in the Security app ', () => { +describe.skip('Kibana navigation to all pages in the Security app ', () => { before(() => { loginAndWaitForPage(KIBANA_HOME); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index 4203b9125d155..096ac0595d76c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -16,6 +16,7 @@ import { TIMELINE_FLYOUT_WRAPPER, TIMELINE_PANEL, TIMELINE_TAB_CONTENT_EQL, + TIMELINE_TAB_CONTENT_GRAPHS_NOTES, } from '../../screens/timeline'; import { createTimelineTemplate } from '../../tasks/api_calls/timelines'; @@ -90,7 +91,9 @@ describe('Timelines', (): void => { it('can be added notes', () => { addNotesToTimeline(getTimeline().notes); - cy.get(NOTES_TEXT).should('have.text', getTimeline().notes); + cy.get(TIMELINE_TAB_CONTENT_GRAPHS_NOTES) + .find(NOTES_TEXT) + .should('have.text', getTimeline().notes); }); it('should update timeline after adding eql', () => { diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 0d6787c49adb3..675a25641a2bd 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -21,15 +21,13 @@ export const ALERT_ID = '[data-test-subj="draggable-content-_id"]'; export const ALERT_RISK_SCORE_HEADER = '[data-test-subj="dataGridHeaderCell-signal.rule.risk_score"]'; -export const ALERT_RULE_METHOD = '[data-test-subj="draggable-content-signal.rule.type"]'; +export const ALERT_RULE_NAME = '[data-test-subj="formatted-field-signal.rule.name"]'; -export const ALERT_RULE_NAME = '[data-test-subj="draggable-content-signal.rule.name"]'; +export const ALERT_RULE_RISK_SCORE = '[data-test-subj="formatted-field-signal.rule.risk_score"]'; -export const ALERT_RULE_RISK_SCORE = '[data-test-subj="draggable-content-signal.rule.risk_score"]'; +export const ALERT_RULE_SEVERITY = '[data-test-subj="formatted-field-signal.rule.severity"]'; -export const ALERT_RULE_SEVERITY = '[data-test-subj="draggable-content-signal.rule.severity"]'; - -export const ALERT_RULE_VERSION = '[data-test-subj="draggable-content-signal.rule.version"]'; +export const ALERT_DATA_GRID = '[data-test-subj="dataGridWrapper"]'; export const CLOSE_ALERT_BTN = '[data-test-subj="close-alert-status"]'; @@ -39,16 +37,16 @@ export const CLOSED_ALERTS_FILTER_BTN = '[data-test-subj="closedAlerts"]'; export const EXPAND_ALERT_BTN = '[data-test-subj="expand-event"]'; -export const IN_PROGRESS_ALERTS_FILTER_BTN = '[data-test-subj="inProgressAlerts"]'; +export const ACKNOWLEDGED_ALERTS_FILTER_BTN = '[data-test-subj="acknowledgedAlerts"]'; export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]'; export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]'; -export const MARK_ALERT_IN_PROGRESS_BTN = '[data-test-subj="in-progress-alert-status"]'; +export const MARK_ALERT_ACKNOWLEDGED_BTN = '[data-test-subj="acknowledged-alert-status"]'; -export const MARK_SELECTED_ALERTS_IN_PROGRESS_BTN = - '[data-test-subj="markSelectedAlertsInProgressButton"]'; +export const MARK_SELECTED_ALERTS_ACKNOWLEDGED_BTN = + '[data-test-subj="markSelectedAlertsAcknowledgedButton"]'; export const NUMBER_OF_ALERTS = '[data-test-subj="events-viewer-panel"] [data-test-subj="server-side-event-count"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index d94be17a0530a..9bc22f35741d9 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -96,7 +96,7 @@ export const TIMELINE_TEMPLATE_DETAILS = 'Timeline template'; export const TIMESTAMP_OVERRIDE_DETAILS = 'Timestamp override'; export const TIMELINE_FIELD = (field: string) => { - return `[data-test-subj="draggable-content-${field}"]`; + return `[data-test-subj="formatted-field-${field}"]`; }; export const getDetails = (title: string) => diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index 0acace399b608..abc3eed4e1418 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -13,11 +13,11 @@ import { CLOSE_SELECTED_ALERTS_BTN, CLOSED_ALERTS_FILTER_BTN, EXPAND_ALERT_BTN, - IN_PROGRESS_ALERTS_FILTER_BTN, + ACKNOWLEDGED_ALERTS_FILTER_BTN, LOADING_ALERTS_PANEL, MANAGE_ALERT_DETECTION_RULES_BTN, - MARK_ALERT_IN_PROGRESS_BTN, - MARK_SELECTED_ALERTS_IN_PROGRESS_BTN, + MARK_ALERT_ACKNOWLEDGED_BTN, + MARK_SELECTED_ALERTS_ACKNOWLEDGED_BTN, OPEN_ALERT_BTN, OPENED_ALERTS_FILTER_BTN, SEND_ALERT_TO_TIMELINE_BTN, @@ -112,18 +112,21 @@ export const openAlerts = () => { cy.get(OPEN_ALERT_BTN).click(); }; -export const goToInProgressAlerts = () => { - cy.get(IN_PROGRESS_ALERTS_FILTER_BTN).click(); +export const goToAcknowledgedAlerts = () => { + cy.get(ACKNOWLEDGED_ALERTS_FILTER_BTN).click(); + cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); + cy.get(REFRESH_BUTTON).should('have.text', 'Refresh'); + cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); }; -export const markInProgressFirstAlert = () => { +export const markAcknowledgedFirstAlert = () => { cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); - cy.get(MARK_ALERT_IN_PROGRESS_BTN).click(); + cy.get(MARK_ALERT_ACKNOWLEDGED_BTN).click(); }; -export const markInProgressAlerts = () => { +export const markAcknowledgedAlerts = () => { cy.get(TAKE_ACTION_POPOVER_BTN).click({ force: true }); - cy.get(MARK_SELECTED_ALERTS_IN_PROGRESS_BTN).click(); + cy.get(MARK_SELECTED_ALERTS_ACKNOWLEDGED_BTN).click(); }; export const selectNumberOfAlerts = (numberOfAlerts: number) => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 47225227485f0..9a500f81ec45f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -394,7 +394,7 @@ export const fillIndexAndIndicatorIndexPattern = ( ) => { getIndexPatternClearButton().click(); getIndicatorIndex().type(`${indexPattern}{enter}`); - getIndicatorIndicatorIndex().type(`${indicatorIndex}{enter}`); + getIndicatorIndicatorIndex().type(`{backspace}{enter}${indicatorIndex}{enter}`); }; export const fillEmailConnectorForm = (connector: EmailConnector = getEmailConnector()) => { @@ -437,7 +437,7 @@ export const getIndexPatternInvalidationText = () => cy.contains(AT_LEAST_ONE_IN export const getAboutContinueButton = () => cy.get(ABOUT_CONTINUE_BTN); /** Returns the continue button on the step of define */ -export const getDefineContinueButton = () => cy.get(DEFINE_CONTINUE_BUTTON); +export const getDefineContinueButton = () => cy.get(DEFINE_CONTINUE_BUTTON).should('exist'); /** Returns the indicator index pattern */ export const getIndicatorIndex = () => cy.get(THREAT_MATCH_INDICATOR_INDEX).eq(0); @@ -447,7 +447,7 @@ export const getIndicatorIndicatorIndex = () => cy.get(THREAT_MATCH_INDICATOR_INDICATOR_INDEX).eq(0); /** Returns the index pattern's clear button */ -export const getIndexPatternClearButton = () => cy.get(COMBO_BOX_CLEAR_BTN).first(); +export const getIndexPatternClearButton = () => cy.get(COMBO_BOX_CLEAR_BTN).should('exist').first(); /** Returns the custom query input */ export const getCustomQueryInput = () => cy.get(THREAT_MATCH_CUSTOM_QUERY_INPUT).eq(0); diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index d487cf6d00ed3..4a61a94e4acea 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -133,15 +133,16 @@ export const goToQueryTab = () => { export const addNotesToTimeline = (notes: string) => { goToNotesTab().then(() => { - cy.get(NOTES_TEXT_AREA).type(notes); - cy.root() - .pipe(($el) => { - $el.find(ADD_NOTE_BUTTON).trigger('click'); - return $el.find(NOTES_TAB_BUTTON).find('.euiBadge'); - }) - .should('have.text', '1'); + cy.get(NOTES_TAB_BUTTON) + .find('.euiBadge__text') + .then(($el) => { + const notesCount = parseInt($el.text(), 10); + + cy.get(NOTES_TEXT_AREA).type(notes); + cy.get(ADD_NOTE_BUTTON).trigger('click'); + cy.get(`${NOTES_TAB_BUTTON} .euiBadge`).should('have.text', `${notesCount + 1}`); + }); }); - goToQueryTab(); goToNotesTab(); }; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 990756f3da701..8bb1f4d75e6bc 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -1,5 +1,9 @@ { "id": "securitySolution", + "owner": { + "name": "Security solution", + "githubTeam": "security-solution" + }, "version": "8.0.0", "extraPublicDirs": ["common"], "kibanaVersion": "kibana", @@ -30,6 +34,7 @@ "security", "spaces", "usageCollection", + "lens", "lists", "home", "telemetry", diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index 0cba9341cbce1..8abe19ed26d8d 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -41,7 +41,10 @@ const StartAppComponent: FC = ({ onAppLeave, store, }) => { - const { i18n } = useKibana().services; + const { + i18n, + application: { capabilities }, + } = useKibana().services; const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); return ( @@ -51,7 +54,7 @@ const StartAppComponent: FC = ({ - + { + if (detLink.id === SecurityPageName.alerts) { + return { + ...detLink, + navLinkStatus: capabilities.siem.read_alerts + ? AppNavLinkStatus.visible + : AppNavLinkStatus.hidden, + searchable: capabilities.siem.read_alerts === true, + }; + } + return detLink; + }), + ] + : [], + }; default: return link; } diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index ddc739b05f4c2..c255702e8de86 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -8,7 +8,6 @@ import React, { useCallback, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { AlertConsumers } from '@kbn/rule-data-utils'; - import { getCaseDetailsUrl, getCaseDetailsUrlWithCommentId, @@ -34,7 +33,6 @@ import { SpyRoute } from '../../../common/utils/route/spy_routes'; import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; import { CaseDetailsRefreshContext } from '../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context'; import { getEndpointDetailsPath } from '../../../management/common/routing'; -import { EntityType } from '../../../../../timelines/common'; interface Props { caseId: string; @@ -55,7 +53,7 @@ export interface CaseProps extends Props { updateCase: (newCase: Case) => void; } -const ALERT_CONSUMER: AlertConsumers[] = [AlertConsumers.SIEM]; +const SECURITY_SOLUTION_ALERT_CONSUMERS: AlertConsumers[] = [AlertConsumers.SIEM]; const TimelineDetailsPanel = ({ alertConsumers }: { alertConsumers?: AlertConsumers[] }) => { const { browserFields, docValueFields } = useSourcererScope(SourcererScopeName.detections); @@ -65,7 +63,7 @@ const TimelineDetailsPanel = ({ alertConsumers }: { alertConsumers?: AlertConsum alertConsumers={alertConsumers} browserFields={browserFields} docValueFields={docValueFields} - entityType={EntityType.ALERTS} + entityType="alerts" isFlyoutView timelineId={TimelineId.casePage} /> @@ -234,7 +232,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = showAlertDetails, subCaseId, timelineIntegration: { - alertConsumers: ALERT_CONSUMER, + alertConsumers: SECURITY_SOLUTION_ALERT_CONSUMERS, editor_plugins: { parsingPlugin: timelineMarkdownPlugin.parser, processingPluginRenderer: timelineMarkdownPlugin.renderer, diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index f7cdf60fdd070..fc440197e8349 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -20,6 +20,7 @@ import { useKibana } from '../../lib/kibana'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; +import type { EntityType } from '../../../../../timelines/common'; export interface OwnProps { end: string; @@ -63,6 +64,7 @@ const defaultAlertsFilters: Filter[] = [ interface Props { timelineId: TimelineIdLiteral; endDate: string; + entityType?: EntityType; startDate: string; pageFilters?: Filter[]; } @@ -70,6 +72,7 @@ interface Props { const AlertsTableComponent: React.FC = ({ timelineId, endDate, + entityType = 'alerts', startDate, pageFilters = [], }) => { @@ -107,7 +110,7 @@ const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} defaultCellActions={defaultCellActions} end={endDate} - entityType="alerts" + entityType={entityType} id={timelineId} renderCellValue={DefaultCellRenderer} rowRenderers={defaultRowRenderers} diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index 36a1e1b941e1b..b0471a72c6ee6 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -25,6 +25,7 @@ const AlertsViewComponent: React.FC = ({ timelineId, deleteQuery, endDate, + entityType, filterQuery, indexNames, pageFilters, @@ -74,6 +75,7 @@ const AlertsViewComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts index 8da6b79e43d2e..3c439a9eff1ea 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts @@ -6,6 +6,7 @@ */ import { Filter } from '../../../../../../../src/plugins/data/public'; +import type { EntityType } from '../../../../../timelines/common'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { HostsComponentsQueryProps } from '../../../hosts/pages/navigation/types'; import { NetworkComponentQueryProps } from '../../../network/pages/navigation/types'; @@ -23,5 +24,6 @@ export interface AlertsComponentsProps stackByOptions?: MatrixHistogramOption[]; defaultFilters?: Filter[]; defaultStackByOption?: MatrixHistogramOption; + entityType?: EntityType; indexNames: string[]; } diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx index a01f22a0942de..878a6de89747b 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx @@ -26,9 +26,6 @@ jest.mock('@elastic/eui', () => { }; }); -const allOthersDataProviderId = - 'draggable-legend-item-527adabe-8e1c-4a1f-965c-2f3d65dda9e1-event_dataset-All others'; - const legendItems: LegendItem[] = [ { color: '#1EA593', @@ -57,12 +54,6 @@ const legendItems: LegendItem[] = [ field: 'event.dataset', value: 'esensor', }, - { - color: '#F37020', - dataProviderId: allOthersDataProviderId, - field: 'event.dataset', - value: 'All others', - }, ]; describe('DraggableLegend', () => { @@ -95,14 +86,7 @@ describe('DraggableLegend', () => { it('renders the legend items', () => { legendItems.forEach((item) => expect( - wrapper - .find( - item.dataProviderId !== allOthersDataProviderId - ? `[data-test-subj="legend-item-${item.dataProviderId}"]` - : '[data-test-subj="all-others-legend-item"]' - ) - .first() - .text() + wrapper.find(`[data-test-subj="legend-item-${item.dataProviderId}"]`).first().text() ).toEqual(item.value) ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx index 175239fcaebe7..cc272e568bce7 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx @@ -26,97 +26,32 @@ jest.mock('@elastic/eui', () => { }); describe('DraggableLegendItem', () => { - describe('rendering a regular (non "All others") legend item', () => { - const legendItem: LegendItem = { - color: '#1EA593', - dataProviderId: - 'draggable-legend-item-3207fda7-d008-402a-86a0-8ad632081bad-event_dataset-flow', - field: 'event.dataset', - value: 'flow', - }; - - let wrapper: ReactWrapper; - - beforeEach(() => { - wrapper = mount( - - - - ); - }); - - it('renders a colored circle with the expected legend item color', () => { - expect(wrapper.find('[data-test-subj="legend-color"]').first().props().color).toEqual( - legendItem.color - ); - }); - - it('renders draggable legend item text', () => { - expect( - wrapper.find(`[data-test-subj="legend-item-${legendItem.dataProviderId}"]`).first().text() - ).toEqual(legendItem.value); - }); - - it('does NOT render a non-draggable "All others" legend item', () => { - expect(wrapper.find(`[data-test-subj="all-others-legend-item"]`).exists()).toBe(false); - }); - }); - - describe('rendering an "All others" legend item', () => { - const allOthersLegendItem: LegendItem = { - color: '#F37020', - dataProviderId: - 'draggable-legend-item-527adabe-8e1c-4a1f-965c-2f3d65dda9e1-event_dataset-All others', - field: 'event.dataset', - value: 'All others', - }; - - let wrapper: ReactWrapper; - - beforeEach(() => { - wrapper = mount( - - - - ); - }); - - it('renders a colored circle with the expected legend item color', () => { - expect(wrapper.find('[data-test-subj="legend-color"]').first().props().color).toEqual( - allOthersLegendItem.color - ); - }); - - it('does NOT render a draggable legend item', () => { - expect( - wrapper - .find(`[data-test-subj="legend-item-${allOthersLegendItem.dataProviderId}"]`) - .exists() - ).toBe(false); - }); - - it('renders NON-draggable `All others` legend item text', () => { - expect(wrapper.find(`[data-test-subj="all-others-legend-item"]`).first().text()).toEqual( - allOthersLegendItem.value - ); - }); - }); + const legendItem: LegendItem = { + color: '#1EA593', + dataProviderId: 'draggable-legend-item-3207fda7-d008-402a-86a0-8ad632081bad-event_dataset-flow', + field: 'event.dataset', + value: 'flow', + }; - it('does NOT render a colored circle when the legend item has no color', () => { - const noColorLegendItem: LegendItem = { - // no `color` attribute for this `LegendItem`! - dataProviderId: - 'draggable-legend-item-3207fda7-d008-402a-86a0-8ad632081bad-event_dataset-flow', - field: 'event.dataset', - value: 'flow', - }; + let wrapper: ReactWrapper; - const wrapper = mount( + beforeEach(() => { + wrapper = mount( - + ); + }); + + it('renders a colored circle with the expected legend item color', () => { + expect(wrapper.find('[data-test-subj="legend-color"]').first().props().color).toEqual( + legendItem.color + ); + }); - expect(wrapper.find('[data-test-subj="legend-color"]').exists()).toBe(false); + it('renders draggable legend item text', () => { + expect( + wrapper.find(`[data-test-subj="legend-item-${legendItem.dataProviderId}"]`).first().text() + ).toEqual(legendItem.value); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx index 493ce4da78eba..b4b12437f8660 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.tsx @@ -7,17 +7,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; import { DefaultDraggable } from '../draggables'; -import * as i18n from './translation'; - -// The "All others" legend item is not draggable -const AllOthers = styled.span` - padding-left: 7px; -`; - export interface LegendItem { color?: string; dataProviderId: string; @@ -41,20 +33,14 @@ const DraggableLegendItemComponent: React.FC<{ )} - {value !== i18n.ALL_OTHERS ? ( - - ) : ( - <> - {value} - - )} + diff --git a/x-pack/plugins/security_solution/public/common/components/charts/translation.ts b/x-pack/plugins/security_solution/public/common/components/charts/translation.ts index f0bf8ef0d0910..a527a85f62c71 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/translation.ts +++ b/x-pack/plugins/security_solution/public/common/components/charts/translation.ts @@ -20,7 +20,3 @@ export const DATA_NOT_AVAILABLE_TITLE = i18n.translate( defaultMessage: 'Chart Data Not Available', } ); - -export const ALL_OTHERS = i18n.translate('xpack.securitySolution.chart.allOthersGroupingLabel', { - defaultMessage: 'All others', -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index a0433f0cc73e1..5a0e0a0633e00 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -67,7 +67,7 @@ export const getColumns = ({ ), sortable: false, truncateText: false, - width: '180px', + width: '132px', render: (values: string[] | null | undefined, data: EventFieldsData) => { const label = data.isObjectArray ? i18n.NESTED_COLUMN(data.field) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx index 020297a322c4f..0756fc8dad88f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -149,6 +149,7 @@ const columns: Array> = [ name: '', }, { + className: 'flyoutOverviewDescription', field: 'description', truncateText: false, render: EnrichmentDescription, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 253406fa9c829..29ba8fc0bd541 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -33,12 +33,23 @@ import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_fe import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import { mockTimelines } from '../../mock/mock_timelines_plugin'; +jest.mock('@kbn/alerts', () => ({ + useGetUserAlertsPermissions: () => ({ + loading: false, + crud: true, + read: true, + }), +})); + jest.mock('../../lib/kibana', () => ({ useKibana: () => ({ services: { application: { navigateToApp: jest.fn(), getUrlForApp: jest.fn(), + capabilities: { + siem: { crud_alerts: true, read_alerts: true }, + }, }, uiSettings: { get: jest.fn(), @@ -337,7 +348,9 @@ describe('EventsViewer', () => { } + headerFilterGroup={ + + } /> ); @@ -350,7 +363,9 @@ describe('EventsViewer', () => { } + headerFilterGroup={ + + } /> ); @@ -365,7 +380,9 @@ describe('EventsViewer', () => { } + headerFilterGroup={ + + } /> ); @@ -380,7 +397,9 @@ describe('EventsViewer', () => { } + headerFilterGroup={ + + } /> ); @@ -395,7 +414,9 @@ describe('EventsViewer', () => { } + headerFilterGroup={ + + } /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 2c4241ffbbb16..d85c4464af986 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -11,7 +11,6 @@ import deepEqual from 'fast-deep-equal'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; @@ -24,7 +23,7 @@ import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; -import { EntityType } from '../../../../../timelines/common'; +import type { EntityType } from '../../../../../timelines/common'; import { TGridCellAction } from '../../../../../timelines/common/types'; import { DetailsPanel } from '../../../timelines/components/side_panel'; import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; @@ -58,7 +57,6 @@ export interface OwnProps { scopeId: SourcererScopeName; start: string; showTotalCount?: boolean; - headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; currentFilter?: Status; onRuleChange?: () => void; @@ -66,12 +64,11 @@ export interface OwnProps { rowRenderers: RowRenderer[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; additionalFilters?: React.ReactNode; + hasAlertsCrud?: boolean; } type Props = OwnProps & PropsFromRedux; -const alertConsumers: AlertConsumers[] = [AlertConsumers.SIEM]; - /** * The stateful events viewer component is the highest level component that is utilized across the security_solution pages layer where * timeline is used BESIDES the flyout. The flyout makes use of the `EventsViewer` component which is a subcomponent here @@ -88,7 +85,6 @@ const StatefulEventsViewerComponent: React.FC = ({ entityType, excludedRowRendererIds, filters, - headerFilterGroup, id, isLive, itemsPerPage, @@ -108,6 +104,7 @@ const StatefulEventsViewerComponent: React.FC = ({ additionalFilters, // If truthy, the graph viewer (Resolver) is showing graphEventId, + hasAlertsCrud = false, }) => { const { timelines: timelinesUi } = useKibana().services; const { @@ -120,6 +117,9 @@ const StatefulEventsViewerComponent: React.FC = ({ const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); // TODO: Once we are past experimental phase this code should be removed const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); + const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled( + 'tGridEventRenderedViewEnabled' + ); useEffect(() => { if (createTimeline != null) { createTimeline({ @@ -153,6 +153,7 @@ const StatefulEventsViewerComponent: React.FC = ({ {tGridEnabled ? ( timelinesUi.getTGrid<'embedded'>({ + id, type: 'embedded', browserFields, columns, @@ -165,8 +166,7 @@ const StatefulEventsViewerComponent: React.FC = ({ filters: globalFilters, globalFullScreen, graphOverlay, - headerFilterGroup, - id, + hasAlertsCrud, indexNames: selectedPatterns, indexPattern, isLive, @@ -186,6 +186,7 @@ const StatefulEventsViewerComponent: React.FC = ({ filterStatus: currentFilter, leadingControlColumns, trailingControlColumns, + tGridEventRenderedViewEnabled, }) ) : ( = ({ end={end} isLoadingIndexPattern={isLoadingIndexPattern} filters={globalFilters} - headerFilterGroup={headerFilterGroup} indexNames={selectedPatterns} indexPattern={indexPattern} isLive={isLive} @@ -219,9 +219,8 @@ const StatefulEventsViewerComponent: React.FC = ({ css` background-color: ${theme.eui.euiColorLightestShade}; padding: ${theme.eui.euiSize}; + .eventFiltersDescriptionList { + margin: ${theme.eui.euiSize} ${theme.eui.euiSize} 0 ${theme.eui.euiSize}; + } + .eventFiltersDescriptionListTitle { + width: 40%; + margin-top: 0; + margin-bottom: ${theme.eui.euiSizeS}; + } + .eventFiltersDescriptionListDescription { + width: 60%; + margin-top: 0; + margin-bottom: ${theme.eui.euiSizeS}; + } `} `; -const MyDescriptionListTitle = styled(EuiDescriptionListTitle)` - width: 40%; -`; - -const MyDescriptionListDescription = styled(EuiDescriptionListDescription)` - width: 60%; +const StyledCommentsSection = styled(EuiFlexItem)` + ${({ theme }) => css` + &&& { + margin: ${theme.eui.euiSizeXS} ${theme.eui.euiSize} ${theme.eui.euiSizeL} ${theme.eui.euiSize}; + } + `} `; const ExceptionDetailsComponent = ({ @@ -77,19 +91,28 @@ const ExceptionDetailsComponent = ({ return ( - + {descriptionListItems.map((item) => ( - {item.title} - - {item.description} - + + + {item.title} + + + + + {item.description} + + ))} - {commentsSection} + {commentsSection} ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx index 7429a934d557d..18b7298136302 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx @@ -15,6 +15,7 @@ import { EuiHideFor, EuiBadge, EuiBadgeGroup, + EuiToolTip, } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled, { css } from 'styled-components'; @@ -26,7 +27,12 @@ import * as i18n from '../../translations'; import { FormattedEntry } from '../../types'; const MyEntriesDetails = styled(EuiFlexItem)` - padding: ${({ theme }) => theme.eui.euiSize}; + ${({ theme }) => css` + padding: ${theme.eui.euiSize} ${theme.eui.euiSizeL} ${theme.eui.euiSizeL} ${theme.eui.euiSizeXS}; + &&& { + margin-left: 0; + } + `} `; const MyEditButton = styled(EuiButton)` @@ -46,8 +52,9 @@ const MyRemoveButton = styled(EuiButton)` `; const MyAndOrBadgeContainer = styled(EuiFlexItem)` - padding-top: ${({ theme }) => theme.eui.euiSizeXL}; - padding-bottom: ${({ theme }) => theme.eui.euiSizeS}; + ${({ theme }) => css` + padding: ${theme.eui.euiSizeXL} ${theme.eui.euiSize} ${theme.eui.euiSizeS} 0; + `} `; const MyActionButton = styled(EuiFlexItem)` @@ -132,7 +139,13 @@ const ExceptionEntriesComponent = ({ ); } else { - return values ?? getEmptyValue(); + return values ? ( + + {values} + + ) : ( + getEmptyValue() + ); } }, }, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx index b73442b04c9b4..6a53f47baf6b8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx @@ -84,7 +84,7 @@ const ExceptionItemComponent = ({ }, [loadingItemIds, exceptionItem.id]); return ( - + diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap index f0fd8427140df..4976c859c00e9 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -181,7 +181,9 @@ exports[`item_details_card ItemDetailsCard should render correctly with no actio exports[`item_details_card ItemDetailsPropertySummary should render correctly 1`] = ` - + name 1 theme.eui.euiSize}; + &&& { + margin-left: 0; + } + padding: ${({ theme }) => theme.eui.euiSizeM} ${({ theme }) => theme.eui.euiSizeL} + ${({ theme }) => theme.eui.euiSizeL} 0; + .trustedAppsConditionsTable { + margin-left: ${({ theme }) => theme.eui.euiSize}; + } `; const DescriptionListTitle = styled(EuiDescriptionListTitle)` &&& { width: 40%; + margin-top: 0; + margin-bottom: ${({ theme }) => theme.eui.euiSizeS}; } `; const DescriptionListDescription = styled(EuiDescriptionListDescription)` &&& { width: 60%; + margin-top: 0; + margin-bottom: ${({ theme }) => theme.eui.euiSizeS}; } `; @@ -80,7 +91,7 @@ interface ItemDetailsPropertySummaryProps { export const ItemDetailsPropertySummary = memo( ({ name, value }) => ( <> - {name} + {name} {value} ) diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx index 372e7fd466b07..17e0262a2cffa 100644 --- a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx @@ -6,8 +6,9 @@ */ import { EuiButtonEmpty } from '@elastic/eui'; -import React, { useRef, useState, useEffect, useCallback, ReactNode } from 'react'; +import React, { useState, useCallback, ReactNode } from 'react'; import styled from 'styled-components'; +import { useIsOverflow } from '../../hooks/use_is_overflow'; import * as i18n from './translations'; const LINE_CLAMP = 3; @@ -39,29 +40,13 @@ const LineClampComponent: React.FC<{ children: ReactNode; lineClampHeight?: number; }> = ({ children, lineClampHeight = LINE_CLAMP_HEIGHT }) => { - const [isOverflow, setIsOverflow] = useState(null); const [isExpanded, setIsExpanded] = useState(null); - const descriptionRef = useRef(null); + const [isOverflow, descriptionRef] = useIsOverflow(children); + const toggleReadMore = useCallback(() => { setIsExpanded((prevState) => !prevState); }, []); - useEffect(() => { - if (descriptionRef?.current?.clientHeight != null) { - if ( - (descriptionRef?.current?.scrollHeight ?? 0) > (descriptionRef?.current?.clientHeight ?? 0) - ) { - setIsOverflow(true); - } - - if ( - (descriptionRef?.current?.scrollHeight ?? 0) <= (descriptionRef?.current?.clientHeight ?? 0) - ) { - setIsOverflow(false); - } - } - }, []); - if (isExpanded) { return ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx index 12084a17e888a..c35d613203f76 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx @@ -5,8 +5,18 @@ * 2.0. */ -import React, { memo, useEffect, useState, useCallback } from 'react'; +import React, { + forwardRef, + memo, + useEffect, + useImperativeHandle, + useRef, + useState, + useCallback, + ElementRef, +} from 'react'; import { EuiMarkdownEditor } from '@elastic/eui'; +import { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context'; import { uiPlugins, parsingPlugins, processingPlugins } from './plugins'; @@ -17,41 +27,64 @@ interface MarkdownEditorProps { editorId?: string; dataTestSubj?: string; height?: number; + autoFocusDisabled?: boolean; } -const MarkdownEditorComponent: React.FC = ({ - onChange, - value, - ariaLabel, - editorId, - dataTestSubj, - height, -}) => { - const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); - const onParse = useCallback((err, { messages }) => { - setMarkdownErrorMessages(err ? [err] : messages); - }, []); - - useEffect( - () => document.querySelector('textarea.euiMarkdownEditorTextArea')?.focus(), - [] - ); - - return ( - - ); -}; +type EuiMarkdownEditorRef = ElementRef; + +export interface MarkdownEditorRef { + textarea: HTMLTextAreaElement | null; + replaceNode: ContextShape['replaceNode']; + toolbar: HTMLDivElement | null; +} + +const MarkdownEditorComponent = forwardRef( + ({ onChange, value, ariaLabel, editorId, dataTestSubj, height, autoFocusDisabled }, ref) => { + const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); + const onParse = useCallback((err, { messages }) => { + setMarkdownErrorMessages(err ? [err] : messages); + }, []); + const editorRef = useRef(null); + + useEffect(() => { + if (!autoFocusDisabled) { + editorRef.current?.textarea?.focus(); + } + }, [autoFocusDisabled]); + + // @ts-expect-error update types + useImperativeHandle(ref, () => { + if (!editorRef.current) { + return null; + } + + const editorNode = editorRef.current?.textarea?.closest('.euiMarkdownEditor'); + + return { + ...editorRef.current, + toolbar: editorNode?.querySelector('.euiMarkdownEditorToolbar'), + }; + }); + + return ( + + ); + } +); + +MarkdownEditorComponent.displayName = 'MarkdownEditorComponent'; export const MarkdownEditor = memo(MarkdownEditorComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx index 1c407b3b8f8c2..82e4d5d5a2600 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import React from 'react'; +import React, { forwardRef } from 'react'; import styled from 'styled-components'; import { EuiMarkdownEditorProps, EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports'; -import { MarkdownEditor } from './editor'; +import { MarkdownEditor, MarkdownEditorRef } from './editor'; type MarkdownEditorFormProps = EuiMarkdownEditorProps & { id: string; @@ -27,40 +27,41 @@ const BottomContentWrapper = styled(EuiFlexGroup)` `} `; -export const MarkdownEditorForm: React.FC = ({ - id, - field, - dataTestSubj, - idAria, - bottomRightContent, -}) => { - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); +export const MarkdownEditorForm = React.memo( + forwardRef( + ({ id, field, dataTestSubj, idAria, bottomRightContent }, ref) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - return ( - - <> - - {bottomRightContent && ( - - {bottomRightContent} - - )} - - - ); -}; + return ( + + <> + + {bottomRightContent && ( + + {bottomRightContent} + + )} + + + ); + } + ) +); + +MarkdownEditorForm.displayName = 'MarkdownEditorForm'; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index 4bd5a43684792..1f98d3b826129 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { renderHook } from '@testing-library/react-hooks'; import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public'; @@ -23,7 +24,7 @@ jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_selector'); jest.mock('../../../hooks/use_experimental_features'); jest.mock('../../../utils/route/use_route_spy'); - +jest.mock('@kbn/alerts'); describe('useSecuritySolutionNavigation', () => { const mockUrlState = { [CONSTANTS.appQuery]: { query: 'host.name:"security-solution-es"', language: 'kuery' }, @@ -75,12 +76,24 @@ describe('useSecuritySolutionNavigation', () => { (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useDeepEqualSelector as jest.Mock).mockReturnValue({ urlState: mockUrlState }); (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy); + (useGetUserAlertsPermissions as jest.Mock).mockReturnValue({ + loading: false, + crud: true, + read: true, + }); + (useKibana as jest.Mock).mockReturnValue({ services: { application: { navigateToApp: jest.fn(), getUrlForApp: (appId: string, options?: { path?: string; deepLinkId?: boolean }) => `${appId}/${options?.deepLinkId ?? ''}${options?.path ?? ''}`, + capabilities: { + siem: { + crud_alerts: true, + read_alerts: true, + }, + }, }, chrome: { setBreadcrumbs: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index feeeacf6124e8..ca574a5872761 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -7,12 +7,15 @@ import React, { useCallback, useMemo } from 'react'; import { EuiSideNavItemType } from '@elastic/eui/src/components/side_nav/side_nav_types'; +import { useGetUserAlertsPermissions } from '@kbn/alerts'; + import { securityNavGroup } from '../../../../app/home/home_navigations'; import { getSearch } from '../helpers'; import { PrimaryNavigationItemsProps } from './types'; -import { useGetUserCasesPermissions } from '../../../lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana'; import { useNavigation } from '../../../lib/kibana/hooks'; import { NavTab } from '../types'; +import { SERVER_APP_ID } from '../../../../../common/constants'; export const usePrimaryNavigationItems = ({ navTabs, @@ -60,7 +63,9 @@ export const usePrimaryNavigationItems = ({ }; function usePrimaryNavigationItemsToDisplay(navTabs: Record) { + const uiCapabilities = useKibana().services.application.capabilities; const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; + const hasAlertsReadPermissions = useGetUserAlertsPermissions(uiCapabilities, SERVER_APP_ID); return useMemo( () => [ { @@ -70,7 +75,9 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { }, { ...securityNavGroup.detect, - items: [navTabs.alerts, navTabs.rules, navTabs.exceptions], + items: hasAlertsReadPermissions.read + ? [navTabs.alerts, navTabs.rules, navTabs.exceptions] + : [navTabs.rules, navTabs.exceptions], }, { ...securityNavGroup.explore, @@ -85,6 +92,6 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { items: [navTabs.endpoints, navTabs.trusted_apps, navTabs.event_filters], }, ], - [navTabs, hasCasesReadPermissions] + [navTabs, hasCasesReadPermissions, hasAlertsReadPermissions] ); } diff --git a/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.test.tsx index e9e235ff3ed9c..938601b3b6b2d 100644 --- a/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.test.tsx @@ -31,6 +31,24 @@ describe('Scroll to top', () => { Object.defineProperty(globalNode.window, 'scroll', { value: null }); Object.defineProperty(globalNode.window, 'scrollTo', { value: spyScrollTo }); mount( useScrollToTop()} />); + expect(spyScrollTo).toHaveBeenCalled(); }); + + test('should not scroll when `shouldScroll` is false', () => { + Object.defineProperty(globalNode.window, 'scroll', { value: spyScroll }); + mount( useScrollToTop(undefined, false)} />); + + expect(spyScrollTo).not.toHaveBeenCalled(); + }); + + test('should scroll the element matching the given selector', () => { + const fakeElement = { scroll: spyScroll }; + Object.defineProperty(globalNode.document, 'querySelector', { + value: () => fakeElement, + }); + mount( useScrollToTop('fake selector')} />); + + expect(spyScroll).toHaveBeenCalledWith(0, 0); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.tsx b/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.tsx index d9f80b7e1c3d2..79e5273b9735e 100644 --- a/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/scroll_to_top/index.tsx @@ -7,14 +7,22 @@ import { useEffect } from 'react'; -export const useScrollToTop = () => { +/** + * containerSelector: The element with scrolling. It defaults to the window. + * shouldScroll: It should be used for conditional scrolling. + */ +export const useScrollToTop = (containerSelector?: string, shouldScroll = true) => { useEffect(() => { + const container = containerSelector ? document.querySelector(containerSelector) : window; + + if (!shouldScroll || !container) return; + // trying to use new API - https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo - if (window.scroll) { - window.scroll(0, 0); + if (container.scroll) { + container.scroll(0, 0); } else { // just a fallback for older browsers - window.scrollTo(0, 0); + container.scrollTo(0, 0); } }); diff --git a/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx b/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx index 4c068675aa5a0..cc1c53d107100 100644 --- a/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx +++ b/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx @@ -32,14 +32,22 @@ EllipsisText.displayName = 'EllipsisText'; interface Props { tooltipContent?: React.ReactNode; children: React.ReactNode; + dataTestSubj?: string; } -export function TruncatableText({ tooltipContent, children, ...props }: Props) { - if (!tooltipContent) return {children}; +export function TruncatableText({ tooltipContent, children, dataTestSubj, ...props }: Props) { + if (!tooltipContent) + return ( + + {children} + + ); return ( - {children} + + {children} + ); } diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index 5a33297f04f9a..fa9de895f7d03 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -7,39 +7,50 @@ import React, { createContext, useContext } from 'react'; import { DeepReadonly } from 'utility-types'; +import { useGetUserAlertsPermissions } from '@kbn/alerts'; + +import { Capabilities } from '../../../../../../../src/core/public'; import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; +import { SERVER_APP_ID } from '../../../../common/constants'; export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; endpointPrivileges: EndpointPrivileges; + alertsPrivileges: ReturnType; } export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, + alertsPrivileges: { loading: false, read: false, crud: false }, }); const UserPrivilegesContext = createContext(initialUserPrivilegesState()); interface UserPrivilegesProviderProps { + kibanaCapabilities: Capabilities; children: React.ReactNode; } -export const UserPrivilegesProvider = ({ children }: UserPrivilegesProviderProps) => { +export const UserPrivilegesProvider = ({ + kibanaCapabilities, + children, +}: UserPrivilegesProviderProps) => { const listPrivileges = useFetchListPrivileges(); const detectionEnginePrivileges = useFetchDetectionEnginePrivileges(); const endpointPrivileges = useEndpointPrivileges(); - + const alertsPrivileges = useGetUserAlertsPermissions(kibanaCapabilities, SERVER_APP_ID); return ( {children} diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_is_overflow.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_is_overflow.tsx new file mode 100644 index 0000000000000..c191b945cc31e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_is_overflow.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useRef, useState } from 'react'; + +/** + * It checks if the element that receives the returned Ref has oveflow the max height. + */ +export const useIsOverflow: ( + dependency: unknown +) => [isOveflow: boolean | null, ref: React.RefObject] = (dependency) => { + const [isOverflow, setIsOverflow] = useState(null); + const ref = useRef(null); + + useEffect(() => { + if (ref.current?.clientHeight != null) { + if ((ref?.current?.scrollHeight ?? 0) > (ref?.current?.clientHeight ?? 0)) { + setIsOverflow(true); + } + + if ((ref.current?.scrollHeight ?? 0) <= (ref?.current?.clientHeight ?? 0)) { + setIsOverflow(false); + } + } + }, [ref, dependency]); + + return [isOverflow, ref]; +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 8130a7058700d..fb772986bc679 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -245,6 +245,7 @@ export const mockGlobalState: State = { id: 'test', savedObjectId: null, columns: defaultHeaders, + defaultColumns: defaultHeaders, indexNames: DEFAULT_INDEX_PATTERN, itemsPerPage: 5, dataProviders: [], diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 647ce4dcd15e8..0c227ac639569 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -14,6 +14,7 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { Store } from 'redux'; import { BehaviorSubject } from 'rxjs'; import { ThemeProvider } from 'styled-components'; +import { Capabilities } from 'src/core/public'; import { createStore, State } from '../store'; import { mockGlobalState } from './global_state'; @@ -73,7 +74,11 @@ const TestProvidersWithPrivilegesComponent: React.FC = ({ ({ eui: euiDarkVars, darkMode: true })}> - + {children} diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index f271f49e56a29..e67b61664745e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -1930,46 +1930,48 @@ export const mockTimelineResults: OpenTimelineResult[] = [ }, ]; +const mockTimelineModelColumns: TimelineModel['columns'] = [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + initialWidth: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + initialWidth: 180, + }, +]; export const mockTimelineModel: TimelineModel = { activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.notes, - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], + columns: mockTimelineModelColumns, + defaultColumns: mockTimelineModelColumns, dataProviders: [], dateRange: { end: '2020-03-18T13:52:38.929Z', @@ -2076,21 +2078,23 @@ export const mockTimelineResult = { stale: false, }; +const defaultTimelineColumns: CreateTimelineProps['timeline']['columns'] = [ + { columnHeaderType: 'not-filtered', id: '@timestamp', type: 'number', initialWidth: 190 }, + { columnHeaderType: 'not-filtered', id: 'message', initialWidth: 180 }, + { columnHeaderType: 'not-filtered', id: 'event.category', initialWidth: 180 }, + { columnHeaderType: 'not-filtered', id: 'event.action', initialWidth: 180 }, + { columnHeaderType: 'not-filtered', id: 'host.name', initialWidth: 180 }, + { columnHeaderType: 'not-filtered', id: 'source.ip', initialWidth: 180 }, + { columnHeaderType: 'not-filtered', id: 'destination.ip', initialWidth: 180 }, + { columnHeaderType: 'not-filtered', id: 'user.name', initialWidth: 180 }, +]; export const defaultTimelineProps: CreateTimelineProps = { from: '2018-11-05T18:58:25.937Z', timeline: { activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, - columns: [ - { columnHeaderType: 'not-filtered', id: '@timestamp', type: 'number', initialWidth: 190 }, - { columnHeaderType: 'not-filtered', id: 'message', initialWidth: 180 }, - { columnHeaderType: 'not-filtered', id: 'event.category', initialWidth: 180 }, - { columnHeaderType: 'not-filtered', id: 'event.action', initialWidth: 180 }, - { columnHeaderType: 'not-filtered', id: 'host.name', initialWidth: 180 }, - { columnHeaderType: 'not-filtered', id: 'source.ip', initialWidth: 180 }, - { columnHeaderType: 'not-filtered', id: 'destination.ip', initialWidth: 180 }, - { columnHeaderType: 'not-filtered', id: 'user.name', initialWidth: 180 }, - ], + columns: defaultTimelineColumns, + defaultColumns: defaultTimelineColumns, dataProviders: [ { and: [], diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts index 0d9e2f4f367ec..b1851fd055b33 100644 --- a/x-pack/plugins/security_solution/public/common/mock/utils.ts +++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts @@ -24,6 +24,8 @@ import { defaultHeaders } from '../../timelines/components/timeline/body/column_ interface Global extends NodeJS.Global { // eslint-disable-next-line @typescript-eslint/no-explicit-any window?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + document?: any; } export const globalNode: Global = global; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx index 2c59868d8a6fe..2c0be879966cc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx @@ -16,7 +16,6 @@ import { DefaultDraggable } from '../../../../common/components/draggables'; import type { GenericBuckets } from '../../../../../common'; import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; import type { AlertsCountAggregation } from './types'; -import { MISSING_IP } from '../common/helpers'; interface AlertsCountProps { loading: boolean; @@ -29,10 +28,6 @@ const Wrapper = styled.div` margin-top: -8px; `; -const StyledSpan = styled.span` - padding-left: 8px; -`; - const getAlertsCountTableColumns = ( selectedStackByOption: string, defaultNumberFormat: string @@ -43,9 +38,7 @@ const getAlertsCountTableColumns = ( name: selectedStackByOption, truncateText: true, render: function DraggableStackOptionField(value: string) { - return value === i18n.ALL_OTHERS || value === MISSING_IP ? ( - {value} - ) : ( + return ( = [] ) => { - const missing = getMissingFields(stackByField); - return { size: 0, aggs: { alertsByGroupingCount: { terms: { field: stackByField, - ...missing, order: { _count: 'desc', }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 001567d7d2cc8..9cc844a80b031 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -14,6 +14,8 @@ import { HeaderSection } from '../../../../common/components/header_section'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { InspectButtonContainer } from '../../../../common/components/inspect'; +import { fetchQueryRuleRegistryAlerts } from '../../../containers/detection_engine/alerts/api'; + import { getAlertsCountQuery } from './helpers'; import * as i18n from './translations'; import { AlertsCount } from './alerts_count'; @@ -42,6 +44,13 @@ export const AlertsCountPanel = memo( DEFAULT_STACK_BY_FIELD ); + // TODO: Once we are past experimental phase this code should be removed + // const fetchMethod = useIsExperimentalFeatureEnabled('ruleRegistryEnabled') + // ? fetchQueryRuleRegistryAlerts + // : fetchQueryAlerts; + + const fetchMethod = fetchQueryRuleRegistryAlerts; + const additionalFilters = useMemo(() => { try { return [ @@ -64,6 +73,7 @@ export const AlertsCountPanel = memo( request, refetch, } = useQueryAlerts<{}, AlertsCountAggregation>({ + fetchMethod, query: getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters), indexName: signalIndexName, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx index 298158440224f..e5534900a3784 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx @@ -10,7 +10,6 @@ import moment from 'moment'; import { isEmpty } from 'lodash/fp'; import type { HistogramData, AlertsAggregation, AlertsBucket, AlertsGroupBucket } from './types'; import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; -import { getMissingFields } from '../common/helpers'; import type { AlertsStackByField } from '../common/types'; const EMPTY_ALERTS_DATA: HistogramData[] = []; @@ -41,14 +40,11 @@ export const getAlertsHistogramQuery = ( bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; }> ) => { - const missing = getMissingFields(stackByField); - return { aggs: { alertsByGrouping: { terms: { field: stackByField, - ...missing, order: { _count: 'desc', }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 2182ed7da0c4f..b296371bae58d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -43,6 +43,7 @@ import type { AlertsStackByField } from '../common/types'; import { KpiPanel, StackBySelect } from '../common/components'; import { useInspectButton } from '../common/hooks'; +import { fetchQueryRuleRegistryAlerts } from '../../../containers/detection_engine/alerts/api'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -116,12 +117,16 @@ export const AlertsHistogramPanel = memo( request, refetch, } = useQueryAlerts<{}, AlertsAggregation>({ - query: getAlertsHistogramQuery( - selectedStackByOption, - from, - to, - buildCombinedQueries(combinedQueries) - ), + fetchMethod: fetchQueryRuleRegistryAlerts, + query: { + index: signalIndexName, + ...getAlertsHistogramQuery( + selectedStackByOption, + from, + to, + buildCombinedQueries(combinedQueries) + ), + }, indexName: signalIndexName, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/helpers.ts deleted file mode 100644 index ecc7cc0197778..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/helpers.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { showAllOthersBucket } from '../../../../../common/constants'; -import type { AlertsStackByField } from './types'; -import * as i18n from './translations'; - -export const MISSING_IP = '0.0.0.0'; - -export const getMissingFields = (stackByField: AlertsStackByField) => - showAllOthersBucket.includes(stackByField) - ? { - missing: stackByField.endsWith('.ip') ? MISSING_IP : i18n.ALL_OTHERS, - } - : {}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts index ef540e088877c..d99e1d4744ae7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/translations.ts @@ -13,10 +13,3 @@ export const STACK_BY_LABEL = i18n.translate( defaultMessage: 'Stack by', } ); - -export const ALL_OTHERS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel', - { - defaultMessage: 'All others', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.test.tsx index c53207d0a8f65..7c0b7cae95d03 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.test.tsx @@ -12,7 +12,9 @@ import { AlertsTableFilterGroup } from './index'; describe('AlertsTableFilterGroup', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find('EuiFilterButton')).toBeTruthy(); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx index 876cc47bff84a..8304bffefb8d0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx @@ -5,84 +5,58 @@ * 2.0. */ -import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; -import { rgba } from 'polished'; -import React, { useCallback, useState } from 'react'; -import styled from 'styled-components'; +import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui'; +import React, { useCallback } from 'react'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import * as i18n from '../translations'; export const FILTER_OPEN: Status = 'open'; export const FILTER_CLOSED: Status = 'closed'; -export const FILTER_IN_PROGRESS: Status = 'in-progress'; - -const StatusFilterButton = styled(EuiFilterButton)<{ $isActive: boolean }>` - background: ${({ $isActive, theme }) => ($isActive ? theme.eui.euiColorPrimary : '')}; -`; - -const StatusFilterGroup = styled(EuiFilterGroup)` - background: ${({ theme }) => rgba(theme.eui.euiColorPrimary, 0.2)}; - .euiButtonEmpty--ghost:enabled:focus { - background-color: ${({ theme }) => theme.eui.euiColorPrimary}; - } -`; +export const FILTER_ACKNOWLEDGED: Status = 'acknowledged'; interface Props { + status: Status; onFilterGroupChanged: (filterGroup: Status) => void; } -const AlertsTableFilterGroupComponent: React.FC = ({ onFilterGroupChanged }) => { - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); - - const onClickOpenFilterCallback = useCallback(() => { - setFilterGroup(FILTER_OPEN); - onFilterGroupChanged(FILTER_OPEN); - }, [setFilterGroup, onFilterGroupChanged]); - - const onClickCloseFilterCallback = useCallback(() => { - setFilterGroup(FILTER_CLOSED); - onFilterGroupChanged(FILTER_CLOSED); - }, [setFilterGroup, onFilterGroupChanged]); - - const onClickInProgressFilterCallback = useCallback(() => { - setFilterGroup(FILTER_IN_PROGRESS); - onFilterGroupChanged(FILTER_IN_PROGRESS); - }, [setFilterGroup, onFilterGroupChanged]); +const AlertsTableFilterGroupComponent: React.FC = ({ + status = FILTER_OPEN, + onFilterGroupChanged, +}) => { + const options: EuiButtonGroupOptionProps[] = [ + { + id: 'open', + label: i18n.OPEN_ALERTS, + 'data-test-subj': 'openAlerts', + }, + { + id: 'acknowledged', + label: i18n.ACKNOWLEDGED_ALERTS, + 'data-test-subj': 'acknowledgedAlerts', + }, + { + id: 'closed', + label: i18n.CLOSED_ALERTS, + 'data-test-subj': 'closedAlerts', + }, + ]; + + const onChange = useCallback( + (id: string) => { + onFilterGroupChanged(id as Status); + }, + [onFilterGroupChanged] + ); return ( - - - {i18n.OPEN_ALERTS} - - - - {i18n.IN_PROGRESS_ALERTS} - - - - {i18n.CLOSED_ALERTS} - - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index 8a88c430b03e9..cba6a3f47f3fc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -27,7 +27,7 @@ import * as i18n from './translations'; import { useUiSetting$ } from '../../../../common/lib/kibana'; import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; import { UpdateAlertsStatus } from '../types'; -import { FILTER_CLOSED, FILTER_IN_PROGRESS, FILTER_OPEN } from '../alerts_filter_group'; +import { FILTER_CLOSED, FILTER_ACKNOWLEDGED, FILTER_OPEN } from '../alerts_filter_group'; export interface AlertsUtilityBarProps { areEventsLoading: boolean; @@ -126,18 +126,18 @@ const AlertsUtilityBarComponent: React.FC = ({ )} - {currentFilter !== FILTER_IN_PROGRESS && ( + {currentFilter !== FILTER_ACKNOWLEDGED && ( { closePopover(); - handleUpdateStatus('in-progress'); + handleUpdateStatus('acknowledged'); }} color="text" - data-test-subj="markSelectedAlertsInProgressButton" + data-test-subj="markSelectedAlertsAcknowledgedButton" > - {i18n.BATCH_ACTION_IN_PROGRESS_SELECTED} + {i18n.BATCH_ACTION_ACKNOWLEDGED_SELECTED} )} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts index c52e443c50753..66fcbae494a41 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts @@ -105,9 +105,9 @@ export const BATCH_ACTION_CLOSE_SELECTED = i18n.translate( } ); -export const BATCH_ACTION_IN_PROGRESS_SELECTED = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.inProgressSelectedTitle', +export const BATCH_ACTION_ACKNOWLEDGED_SELECTED = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.batchActions.acknowledgedSelectedTitle', { - defaultMessage: 'Mark in progress', + defaultMessage: 'Mark as acknowledged', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 0519e3f2d4a75..75bd41037934b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -13,7 +13,6 @@ import { ALERT_STATUS, ALERT_UUID, ALERT_RULE_UUID, - ALERT_RULE_ID, ALERT_RULE_NAME, ALERT_RULE_CATEGORY, } from '@kbn/rule-data-utils'; @@ -190,7 +189,6 @@ export const requiredFieldMappingsForActionsRuleRegistry = { 'alert.status': ALERT_STATUS, 'alert.duration.us': ALERT_DURATION, 'rule.uuid': ALERT_RULE_UUID, - 'rule.id': ALERT_RULE_ID, 'rule.name': ALERT_RULE_NAME, 'rule.category': ALERT_RULE_CATEGORY, producer: ALERT_RULE_PRODUCER, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index dec8e3e83a1ab..fc3e1e7f2d69b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -174,7 +174,8 @@ export const AlertsTableComponent: React.FC = ({ title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated); break; case 'in-progress': - title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated); + case 'acknowledged': + title = i18n.ACKNOWLEDGED_ALERT_SUCCESS_TOAST(updated); } displaySuccessToast(title, dispatchToaster); } @@ -193,7 +194,8 @@ export const AlertsTableComponent: React.FC = ({ title = i18n.OPENED_ALERT_FAILED_TOAST; break; case 'in-progress': - title = i18n.IN_PROGRESS_ALERT_FAILED_TOAST; + case 'acknowledged': + title = i18n.ACKNOWLEDGED_ALERT_FAILED_TOAST; } displayErrorToast(title, [error.message], dispatchToaster); }, @@ -394,6 +396,7 @@ export const AlertsTableComponent: React.FC = ({ start={from} utilityBar={utilityBarCallback} additionalFilters={additionalFiltersComponent} + hasAlertsCrud={hasIndexWrite} /> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx index 7be51c4eaa41a..6639a7f3129c9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx @@ -26,6 +26,7 @@ const AddEndpointExceptionComponent: React.FC = ({ id="addEndpointException" onClick={onClick} disabled={disabled} + size="s" > {i18n.ACTION_ADD_ENDPOINT_EXCEPTION}
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx index 99eef3aefd42c..af3d15184a686 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx @@ -23,6 +23,7 @@ const AddExceptionComponent: React.FC = ({ disabled, onClick id="addException" onClick={onClick} disabled={disabled} + size="s" > {i18n.ACTION_ADD_EXCEPTION} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index b49c5602bc140..101ba99f0bba6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -13,6 +13,14 @@ import React from 'react'; import { Ecs } from '../../../../../common/ecs'; import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin'; +jest.mock('@kbn/alerts', () => ({ + useGetUserAlertsPermissions: () => ({ + loading: false, + crud: true, + read: true, + }), +})); + const ecsRowData: Ecs = { _id: '1', agent: { type: ['blah'] } }; const props = { @@ -36,6 +44,9 @@ jest.mock('../../../../common/lib/kibana', () => ({ useKibana: () => ({ services: { timelines: { ...mockTimelines }, + application: { + capabilities: { siem: { crud_alerts: true, read_alerts: true } }, + }, }, }), useGetUserCasesPermissions: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 9155d38ba315b..c6243b0e8d709 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -184,6 +184,7 @@ const AlertContextMenuComponent: React.FC = ({ eventId: ecsRowData?._id, indexName: ecsRowData?._index ?? '', timelineId, + refetch, closePopover, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/acknowledged_alert_status.tsx similarity index 55% rename from x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx rename to x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/acknowledged_alert_status.tsx index f273833c1c1b3..1c97b304a74a3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/acknowledged_alert_status.tsx @@ -7,30 +7,30 @@ import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; -import { FILTER_IN_PROGRESS } from '../../alerts_filter_group'; +import { FILTER_ACKNOWLEDGED } from '../../alerts_filter_group'; import * as i18n from '../../translations'; -interface InProgressAlertStatusProps { +interface AcknowledgedAlertStatusProps { onClick: () => void; disabled?: boolean; } -const InProgressAlertStatusComponent: React.FC = ({ +const AcknowledgedAlertStatusComponent: React.FC = ({ onClick, disabled, }) => { return ( - {i18n.ACTION_IN_PROGRESS_ALERT} + {i18n.ACTION_ACKNOWLEDGED_ALERT} ); }; -export const InProgressAlertStatus = React.memo(InProgressAlertStatusComponent); +export const AcknowledgedAlertStatus = React.memo(AcknowledgedAlertStatusComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx index f06d671549357..780cb65ed13d3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx @@ -7,27 +7,21 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; +import { useGetUserAlertsPermissions } from '@kbn/alerts'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { timelineActions } from '../../../../timelines/store/timeline'; import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; -import * as i18nCommon from '../../../../common/translations'; -import * as i18n from '../translations'; - -import { - useStateToaster, - displaySuccessToast, - displayErrorToast, -} from '../../../../common/components/toasters'; import { useStatusBulkActionItems } from '../../../../../../timelines/public'; - +import { useKibana } from '../../../../common/lib/kibana'; +import { SERVER_APP_ID } from '../../../../../common/constants'; interface Props { alertStatus?: Status; closePopover: () => void; eventId: string; timelineId: string; indexName: string; + refetch?: () => void; } export const useAlertsActions = ({ @@ -36,59 +30,18 @@ export const useAlertsActions = ({ eventId, timelineId, indexName, + refetch, }: Props) => { const dispatch = useDispatch(); - const [, dispatchToaster] = useStateToaster(); - - const { addWarning } = useAppToasts(); - - const onAlertStatusUpdateSuccess = useCallback( - (updated: number, conflicts: number, newStatus: Status) => { - closePopover(); - if (conflicts > 0) { - // Partial failure - addWarning({ - title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts), - text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts), - }); - } else { - let title: string; - switch (newStatus) { - case 'closed': - title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated); - break; - case 'open': - title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated); - break; - case 'in-progress': - title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated); - } - - displaySuccessToast(title, dispatchToaster); - } - }, - [addWarning, closePopover, dispatchToaster] - ); - - const onAlertStatusUpdateFailure = useCallback( - (newStatus: Status, error: Error) => { - let title: string; - closePopover(); + const uiCapabilities = useKibana().services.application.capabilities; + const alertsPrivileges = useGetUserAlertsPermissions(uiCapabilities, SERVER_APP_ID); - switch (newStatus) { - case 'closed': - title = i18n.CLOSED_ALERT_FAILED_TOAST; - break; - case 'open': - title = i18n.OPENED_ALERT_FAILED_TOAST; - break; - case 'in-progress': - title = i18n.IN_PROGRESS_ALERT_FAILED_TOAST; - } - displayErrorToast(title, [error.message], dispatchToaster); - }, - [closePopover, dispatchToaster] - ); + const onStatusUpdate = useCallback(() => { + closePopover(); + if (refetch) { + refetch(); + } + }, [closePopover, refetch]); const setEventsLoading = useCallback( ({ eventIds, isLoading }: SetEventsLoadingProps) => { @@ -110,11 +63,11 @@ export const useAlertsActions = ({ indexName, setEventsLoading, setEventsDeleted, - onUpdateSuccess: onAlertStatusUpdateSuccess, - onUpdateFailure: onAlertStatusUpdateFailure, + onUpdateSuccess: onStatusUpdate, + onUpdateFailure: onStatusUpdate, }); return { - actionItems, + actionItems: alertsPrivileges.crud ? actionItems : [], }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index ac768cf4c929d..c6d9cd3eef7a3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -38,10 +38,10 @@ export const CLOSED_ALERTS = i18n.translate( } ); -export const IN_PROGRESS_ALERTS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.inProgressAlertsTitle', +export const ACKNOWLEDGED_ALERTS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.acknowledgedAlertsTitle', { - defaultMessage: 'In progress', + defaultMessage: 'Acknowledged', } ); @@ -150,10 +150,10 @@ export const ACTION_CLOSE_ALERT = i18n.translate( } ); -export const ACTION_IN_PROGRESS_ALERT = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.actions.inProgressAlertTitle', +export const ACTION_ACKNOWLEDGED_ALERT = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.acknowledgedAlertTitle', { - defaultMessage: 'Mark in progress', + defaultMessage: 'Mark as acknowledged', } ); @@ -220,13 +220,13 @@ export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => 'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.', }); -export const IN_PROGRESS_ALERT_SUCCESS_TOAST = (totalAlerts: number) => +export const ACKNOWLEDGED_ALERT_SUCCESS_TOAST = (totalAlerts: number) => i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.inProgressAlertSuccessToastMessage', + 'xpack.securitySolution.detectionEngine.alerts.acknowledgedAlertSuccessToastMessage', { values: { totalAlerts }, defaultMessage: - 'Successfully marked {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}} as in progress.', + 'Successfully marked {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}} as acknowledged.', } ); @@ -244,10 +244,10 @@ export const OPENED_ALERT_FAILED_TOAST = i18n.translate( } ); -export const IN_PROGRESS_ALERT_FAILED_TOAST = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.inProgressAlertFailedToastMessage', +export const ACKNOWLEDGED_ALERT_FAILED_TOAST = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.acknowledgedAlertFailedToastMessage', { - defaultMessage: 'Failed to mark alert(s) as in progress', + defaultMessage: 'Failed to mark alert(s) as acknowledged', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 4487455c11a00..c40821b1b2949 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -159,6 +159,7 @@ export const TakeActionDropdown = React.memo( eventId: actionsData.eventId, indexName, timelineId, + refetch, closePopover: closePopoverAndFlyout, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index bb9ec01399f8d..9972233dce351 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useUserInfo, ManageUserInfo } from './index'; +import { Capabilities } from 'src/core/public'; import { useKibana } from '../../../common/lib/kibana'; import * as api from '../../containers/detection_engine/alerts/api'; @@ -46,6 +47,7 @@ describe('useUserInfo', () => { hasIndexManage: null, hasIndexMaintenance: null, hasIndexWrite: null, + hasIndexRead: null, hasIndexUpdateDelete: null, isAuthenticated: null, isSignalIndexExists: null, @@ -65,7 +67,11 @@ describe('useUserInfo', () => { }); const wrapper = ({ children }: { children: JSX.Element }) => ( - + {children} diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index 9a8fc5e27a5e4..da6df631d951e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -18,6 +18,7 @@ export interface State { hasIndexManage: boolean | null; hasIndexMaintenance: boolean | null; hasIndexWrite: boolean | null; + hasIndexRead: boolean | null; hasIndexUpdateDelete: boolean | null; isSignalIndexExists: boolean | null; isAuthenticated: boolean | null; @@ -32,6 +33,7 @@ export const initialState: State = { hasIndexManage: null, hasIndexMaintenance: null, hasIndexWrite: null, + hasIndexRead: null, hasIndexUpdateDelete: null, isSignalIndexExists: null, isAuthenticated: null, @@ -55,6 +57,10 @@ export type Action = type: 'updateHasIndexWrite'; hasIndexWrite: boolean | null; } + | { + type: 'updateHasIndexRead'; + hasIndexRead: boolean | null; + } | { type: 'updateHasIndexUpdateDelete'; hasIndexUpdateDelete: boolean | null; @@ -110,6 +116,12 @@ export const userInfoReducer = (state: State, action: Action): State => { hasIndexWrite: action.hasIndexWrite, }; } + case 'updateHasIndexRead': { + return { + ...state, + hasIndexRead: action.hasIndexRead, + }; + } case 'updateHasIndexUpdateDelete': { return { ...state, @@ -178,6 +190,7 @@ export const useUserInfo = (): State => { hasIndexManage, hasIndexMaintenance, hasIndexWrite, + hasIndexRead, hasIndexUpdateDelete, isSignalIndexExists, isAuthenticated, @@ -194,8 +207,9 @@ export const useUserInfo = (): State => { hasEncryptionKey: isApiEncryptionKey, hasIndexManage: hasApiIndexManage, hasIndexMaintenance: hasApiIndexMaintenance, - hasIndexWrite: hasApiIndexWrite, hasIndexUpdateDelete: hasApiIndexUpdateDelete, + hasIndexWrite: hasApiIndexWrite, + hasIndexRead: hasApiIndexRead, } = useAlertsPrivileges(); const { loading: indexNameLoading, @@ -228,6 +242,12 @@ export const useUserInfo = (): State => { } }, [dispatch, loading, hasIndexWrite, hasApiIndexWrite]); + useEffect(() => { + if (!loading && hasIndexRead !== hasApiIndexRead && hasApiIndexRead != null) { + dispatch({ type: 'updateHasIndexRead', hasIndexRead: hasApiIndexRead }); + } + }, [dispatch, loading, hasIndexRead, hasApiIndexRead]); + useEffect(() => { if ( !loading && @@ -334,6 +354,7 @@ export const useUserInfo = (): State => { hasIndexManage, hasIndexMaintenance, hasIndexWrite, + hasIndexRead, hasIndexUpdateDelete, signalIndexName, signalIndexMappingOutdated, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index 3d4a7dba0de57..88882131fed03 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -13,6 +13,7 @@ import { DETECTION_ENGINE_SIGNALS_STATUS_URL, DETECTION_ENGINE_INDEX_URL, DETECTION_ENGINE_PRIVILEGES_URL, + ALERTS_AS_DATA_FIND_URL, } from '../../../../../common/constants'; import { HOST_METADATA_GET_ROUTE } from '../../../../../common/endpoint/constants'; import { KibanaServices } from '../../../../common/lib/kibana'; @@ -39,8 +40,8 @@ import { resolvePathVariables } from '../../../../common/utils/resolve_path_vari export const fetchQueryAlerts = async ({ query, signal, -}: QueryAlerts): Promise> => - KibanaServices.get().http.fetch>( +}: QueryAlerts): Promise> => { + return KibanaServices.get().http.fetch>( DETECTION_ENGINE_QUERY_SIGNALS_URL, { method: 'POST', @@ -48,12 +49,35 @@ export const fetchQueryAlerts = async ({ signal, } ); +}; + +/** + * Fetch Alerts by providing a query + * + * @param query String to match a dsl + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchQueryRuleRegistryAlerts = async ({ + query, + signal, +}: QueryAlerts): Promise> => { + return KibanaServices.get().http.fetch>( + ALERTS_AS_DATA_FIND_URL, + { + method: 'POST', + body: JSON.stringify(query), + signal, + } + ); +}; /** * Update alert status by query * * @param query of alerts to update - * @param status to update to('open' / 'closed' / 'in-progress') + * @param status to update to('open' / 'closed' / 'acknowledged') * @param signal AbortSignal for cancelling request * * @throws An error if response is not OK diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index f3afe83365286..64d9db80316a9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -87,6 +87,7 @@ const userPrivilegesInitial: ReturnType = { error: undefined, }, endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, + alertsPrivileges: { loading: true, crud: false, read: false }, }; describe('usePrivilegeUser', () => { @@ -161,8 +162,8 @@ describe('usePrivilegeUser', () => { hasEncryptionKey: true, hasIndexManage: false, hasIndexMaintenance: true, - hasIndexRead: true, - hasIndexWrite: true, + hasIndexRead: false, + hasIndexWrite: false, hasIndexUpdateDelete: true, isAuthenticated: true, loading: false, @@ -186,8 +187,8 @@ describe('usePrivilegeUser', () => { hasEncryptionKey: true, hasIndexManage: true, hasIndexMaintenance: true, - hasIndexRead: true, - hasIndexWrite: true, + hasIndexRead: false, + hasIndexWrite: false, hasIndexUpdateDelete: true, isAuthenticated: true, loading: false, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx index 005224a80c189..1d9b8228b5070 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx @@ -35,7 +35,7 @@ export const useAlertsPrivileges = (): UseAlertsPrivelegesReturn => { hasIndexUpdateDelete: null, hasIndexMaintenance: null, }); - const { detectionEnginePrivileges } = useUserPrivileges(); + const { detectionEnginePrivileges, alertsPrivileges } = useUserPrivileges(); useEffect(() => { if (detectionEnginePrivileges.error != null) { @@ -62,17 +62,13 @@ export const useAlertsPrivileges = (): UseAlertsPrivelegesReturn => { hasEncryptionKey: privilege.has_encryption_key, hasIndexManage: privilege.index[indexName].manage && privilege.cluster.manage, hasIndexMaintenance: privilege.index[indexName].maintenance, - hasIndexRead: privilege.index[indexName].read, - hasIndexWrite: - privilege.index[indexName].create || - privilege.index[indexName].create_doc || - privilege.index[indexName].index || - privilege.index[indexName].write, + hasIndexRead: alertsPrivileges.read, + hasIndexWrite: alertsPrivileges.crud, hasIndexUpdateDelete: privilege.index[indexName].write, }); } } - }, [detectionEnginePrivileges.result]); + }, [detectionEnginePrivileges.result, alertsPrivileges]); return { loading: detectionEnginePrivileges.loading, ...privileges }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx index 4d7d80b74a24d..b2bbcdf277992 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx @@ -8,7 +8,7 @@ import { isEmpty } from 'lodash'; import React, { SetStateAction, useEffect, useState } from 'react'; -import { fetchQueryAlerts } from './api'; +import { fetchQueryAlerts, fetchQueryRuleRegistryAlerts } from './api'; import { AlertSearchResponse } from './types'; type Func = () => Promise; @@ -23,6 +23,7 @@ export interface ReturnQueryAlerts { } interface AlertsQueryParams { + fetchMethod?: typeof fetchQueryAlerts | typeof fetchQueryRuleRegistryAlerts; query: object; indexName?: string | null; skip?: boolean; @@ -35,6 +36,7 @@ interface AlertsQueryParams { * */ export const useQueryAlerts = ({ + fetchMethod = fetchQueryAlerts, query: initialQuery, indexName, skip, @@ -58,7 +60,8 @@ export const useQueryAlerts = ({ const fetchData = async () => { try { setLoading(true); - const alertResponse = await fetchQueryAlerts({ + + const alertResponse = await fetchMethod({ query, signal: abortCtrl.signal, }); @@ -95,7 +98,7 @@ export const useQueryAlerts = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [query, indexName, skip]); + }, [query, indexName, skip, fetchMethod]); return { loading, ...alerts }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index ade83fed4fd6b..6d68dae375866 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -14,6 +14,13 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('@kbn/alerts', () => ({ + useGetUserAlertsPermissions: () => ({ + loading: false, + crud: true, + read: true, + }), +})); describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx new file mode 100644 index 0000000000000..dbd59d2510238 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { useGetUserAlertsPermissions } from '@kbn/alerts'; + +import { ALERTS_PATH, SecurityPageName, SERVER_APP_ID } from '../../../../common/constants'; +import { NotFoundPage } from '../../../app/404'; +import * as i18n from './translations'; +import { TrackApplicationView } from '../../../../../../../src/plugins/usage_collection/public'; +import { DetectionEnginePage } from '../../pages/detection_engine/detection_engine'; +import { useKibana } from '../../../common/lib/kibana'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; + +const AlertsRoute = () => ( + + + + +); + +const AlertsContainerComponent: React.FC = () => { + const { + chrome, + application: { capabilities }, + } = useKibana().services; + const userPermissions = useGetUserAlertsPermissions(capabilities, SERVER_APP_ID); + + useEffect(() => { + // if the user is read only then display the glasses badge in the global navigation header + if (userPermissions != null && !userPermissions.crud && userPermissions.read) { + chrome.setBadge({ + text: i18n.READ_ONLY_BADGE_TEXT, + tooltip: i18n.READ_ONLY_BADGE_TOOLTIP, + iconType: 'glasses', + }); + } + + // remove the icon after the component unmounts + return () => { + chrome.setBadge(); + }; + }, [userPermissions, chrome]); + + return ( + + + + + ); +}; + +export const Alerts = React.memo(AlertsContainerComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts new file mode 100644 index 0000000000000..734e93925e536 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const READ_ONLY_BADGE_TEXT = i18n.translate( + 'xpack.securitySolution.alerts.badge.readOnly.text', + { + defaultMessage: 'Read only', + } +); + +export const READ_ONLY_BADGE_TOOLTIP = i18n.translate( + 'xpack.securitySolution.alerts.badge.readOnly.tooltip', + { + defaultMessage: 'Unable to update alerts', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index badad82a2f760..a92f4d706dc7c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -67,6 +67,9 @@ jest.mock('../../../common/lib/kibana', () => { services: { application: { navigateToUrl: jest.fn(), + capabilities: { + siem: { crud_alerts: true, read_alerts: true }, + }, }, timelines: { ...mockTimelines }, data: { @@ -74,6 +77,13 @@ jest.mock('../../../common/lib/kibana', () => { filterManager: jest.fn().mockReturnValue({}), }, }, + docLinks: { + links: { + siem: { + gettingStarted: 'link', + }, + }, + }, }, }), useToasts: jest.fn().mockReturnValue({ @@ -94,7 +104,11 @@ const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage) describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); - (useUserData as jest.Mock).mockReturnValue([{}]); + (useUserData as jest.Mock).mockReturnValue([ + { + hasIndexRead: true, + }, + ]); (useSourcererScope as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 838f0030defe1..d6531198c1884 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -17,6 +17,8 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; +import { AlertsFeatureNoPermissions } from '@kbn/alerts'; + import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { isTab } from '../../../../../timelines/public'; @@ -112,12 +114,11 @@ const DetectionEnginePageComponent: React.FC = ({ const [ { loading: userInfoLoading, - isSignalIndexExists, isAuthenticated: isUserAuthenticated, hasEncryptionKey, signalIndexName, hasIndexWrite, - hasIndexMaintenance, + hasIndexRead, }, ] = useUserData(); const { @@ -131,6 +132,7 @@ const DetectionEnginePageComponent: React.FC = ({ const { application: { navigateToUrl }, timelines: timelinesUi, + docLinks, } = useKibana().services; const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); @@ -256,12 +258,12 @@ const DetectionEnginePageComponent: React.FC = ({ ); } - if (!loading && (isSignalIndexExists === false || needsListsConfiguration)) { + if (!loading && (indicesExist === false || needsListsConfiguration)) { return ( @@ -276,74 +278,89 @@ const DetectionEnginePageComponent: React.FC = ({ {indicesExist ? ( - - - - - - - - - {i18n.BUTTON_MANAGE_RULES} - - - - - - - - - {timelinesUi.getLastUpdated({ updatedAt: updatedAt || 0, showUpdating: loading })} - - - - - - - + {hasIndexRead ? ( + <> + + + + + + + + {i18n.BUTTON_MANAGE_RULES} + + + + + + + + + {timelinesUi.getLastUpdated({ + updatedAt: updatedAt || 0, + showUpdating: loading, + })} + + + + + + + - - - - + + + + - - + + - + + + ) : ( + - + )} ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 01867e4e53d5c..c1d674ce456ff 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -45,7 +45,13 @@ jest.mock('../../../../../common/containers/use_global_time', () => ({ setQuery: jest.fn(), }), })); - +jest.mock('@kbn/alerts', () => ({ + useGetUserAlertsPermissions: () => ({ + loading: false, + crud: true, + read: true, + }), +})); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -65,8 +71,12 @@ jest.mock('../../../../../common/lib/kibana', () => { useKibana: () => ({ services: { application: { + ...original.useKibana().services.application, navigateToUrl: jest.fn(), - capabilities: { actions: jest.fn().mockReturnValue({}) }, + capabilities: { + actions: jest.fn().mockReturnValue({}), + siem: { crud_alerts: true, read_alerts: true }, + }, }, timelines: { ...mockTimelines }, data: { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index ffba80d165ac5..4c3db2ae62be3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -197,6 +197,7 @@ const RuleDetailsPageComponent: React.FC = ({ hasEncryptionKey, canUserCRUD, hasIndexWrite, + hasIndexRead, hasIndexMaintenance, signalIndexName, }, @@ -227,6 +228,7 @@ const RuleDetailsPageComponent: React.FC = ({ // This is used to re-trigger api rule status when user de/activate rule const [ruleEnabled, setRuleEnabled] = useState(null); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.alerts); + const [pageTabs, setTabs] = useState(ruleDetailTabs); const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = rule != null ? getStepsData({ rule, detailsView: true }) @@ -273,6 +275,16 @@ const RuleDetailsPageComponent: React.FC = ({ } }, [maybeRule]); + useEffect(() => { + if (!hasIndexRead) { + setTabs(ruleDetailTabs.filter(({ id }) => id !== RuleDetailTabs.alerts)); + setRuleDetailTab(RuleDetailTabs.exceptions); + } else { + setTabs(ruleDetailTabs); + setRuleDetailTab(RuleDetailTabs.alerts); + } + }, [hasIndexRead]); + const title = useMemo( () => ( <> @@ -395,7 +407,7 @@ const RuleDetailsPageComponent: React.FC = ({ const tabs = useMemo( () => ( - {ruleDetailTabs.map((tab) => ( + {pageTabs.map((tab) => ( setRuleDetailTab(tab.id)} isSelected={tab.id === ruleDetailTab} @@ -408,7 +420,7 @@ const RuleDetailsPageComponent: React.FC = ({ ))} ), - [ruleDetailTab, setRuleDetailTab] + [ruleDetailTab, setRuleDetailTab, pageTabs] ); const ruleIndices = useMemo( () => @@ -457,7 +469,7 @@ const RuleDetailsPageComponent: React.FC = ({ ); } else if ( currentStatus?.status === 'failed' && - ruleDetailTab === RuleDetailTabs.alerts && + (ruleDetailTab === RuleDetailTabs.alerts || ruleDetailTab === RuleDetailTabs.failures) && currentStatus?.last_failure_at != null ) { return ( @@ -468,7 +480,7 @@ const RuleDetailsPageComponent: React.FC = ({ ); } else if ( (currentStatus?.status === 'warning' || currentStatus?.status === 'partial failure') && - ruleDetailTab === RuleDetailTabs.alerts && + (ruleDetailTab === RuleDetailTabs.alerts || ruleDetailTab === RuleDetailTabs.failures) && currentStatus?.last_success_at != null ) { return ( @@ -750,11 +762,14 @@ const RuleDetailsPageComponent: React.FC = ({ {tabs} - {ruleDetailTab === RuleDetailTabs.alerts && ( + {ruleDetailTab === RuleDetailTabs.alerts && hasIndexRead && ( <> - + {timelinesUi.getLastUpdated({ diff --git a/x-pack/plugins/security_solution/public/detections/routes.tsx b/x-pack/plugins/security_solution/public/detections/routes.tsx index f0128577cb268..5b7b85c0183aa 100644 --- a/x-pack/plugins/security_solution/public/detections/routes.tsx +++ b/x-pack/plugins/security_solution/public/detections/routes.tsx @@ -6,28 +6,11 @@ */ import React from 'react'; -import { Redirect, RouteProps, RouteComponentProps, Route, Switch } from 'react-router-dom'; -import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; -import { ALERTS_PATH, DETECTIONS_PATH, SecurityPageName } from '../../common/constants'; -import { NotFoundPage } from '../app/404'; +import { Redirect, RouteProps, RouteComponentProps } from 'react-router-dom'; +import { ALERTS_PATH, DETECTIONS_PATH } from '../../common/constants'; +import { Alerts } from './pages/alerts'; -import { SpyRoute } from '../common/utils/route/spy_routes'; - -import { DetectionEnginePage } from './pages/detection_engine/detection_engine'; - -const AlertsRoute = () => ( - - - - -); - -const renderAlertsRoutes = () => ( - - - - -); +const renderAlertsRoutes = () => ; const DetectionsRedirects = ({ location }: RouteComponentProps) => location.pathname === DETECTIONS_PATH ? ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx index 5dcfdba63b9e9..2d83efa7b0bd2 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx @@ -52,6 +52,7 @@ export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQu return ( } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 8466e19100f73..e206f85df6548 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -11,7 +11,7 @@ import { Dispatch } from 'redux'; import { useHistory, useLocation } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiSpacer, EuiHorizontalRule, EuiText } from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; @@ -192,7 +192,7 @@ export const EventFiltersListPage = memo(() => { title={ } subtitle={ABOUT_EVENT_FILTERS} @@ -207,7 +207,7 @@ export const EventFiltersListPage = memo(() => { > ) @@ -236,11 +236,11 @@ export const EventFiltersListPage = memo(() => { - + )} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts index 4c127ee47003f..ae8012711fbf1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts @@ -54,6 +54,5 @@ export const getGetErrorMessage = (getError: ServerApiError) => { export const ABOUT_EVENT_FILTERS = i18n.translate('xpack.securitySolution.eventFilters.aboutInfo', { defaultMessage: - 'Add an event filter to exclude high volume or unwanted events from being written to Elasticsearch. Event ' + - 'filters are processed by the Endpoint Security integration, and are applied to hosts running this integration on their agents.', + 'Add an event filter to exclude high volume or unwanted events from being written to Elasticsearch.', }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx index a77d661593e6b..8bfda22fd3701 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx @@ -28,7 +28,7 @@ export const BehaviorProtection = React.memo(() => { const protectionLabel = i18n.translate( 'xpack.securitySolution.endpoint.policy.protections.behavior', { - defaultMessage: 'Behaviour protections', + defaultMessage: 'Behavior protections', } ); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 190b78761a9de..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,109 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`control_panel ControlPanel should render grid selection correctly 1`] = ` - - - - 0 trusted applications - - - - - - -`; - -exports[`control_panel ControlPanel should render list selection correctly 1`] = ` - - - - 0 trusted applications - - - - - - -`; - -exports[`control_panel ControlPanel should render plural count correctly 1`] = ` - - - - 100 trusted applications - - - - - - -`; - -exports[`control_panel ControlPanel should render singular count correctly 1`] = ` - - - - 1 trusted application - - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.stories.tsx deleted file mode 100644 index 341017dd6d718..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.stories.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { ThemeProvider } from 'styled-components'; -import { storiesOf, addDecorator } from '@storybook/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; - -import { ControlPanel, ControlPanelProps } from '.'; -import { ViewType } from '../../../state'; - -addDecorator((storyFn) => ( - ({ eui: euiLightVars, darkMode: false })}>{storyFn()} -)); - -const useRenderStory = (props: Omit) => { - const [selectedOption, setSelectedOption] = useState(props.currentViewType); - - return ( - - ); -}; - -storiesOf('TrustedApps/ControlPanel', module) - .add('list view selected', () => { - return useRenderStory({ totalItemCount: 0, currentViewType: 'list' }); - }) - .add('plural totals', () => { - return useRenderStory({ totalItemCount: 200, currentViewType: 'grid' }); - }) - .add('singular totals', () => { - return useRenderStory({ totalItemCount: 1, currentViewType: 'grid' }); - }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.test.tsx deleted file mode 100644 index 5530e15f981e9..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import { shallow } from 'enzyme'; -import React from 'react'; - -import { ControlPanel } from '.'; - -describe('control_panel', () => { - describe('ControlPanel', () => { - it('should render grid selection correctly', () => { - const element = shallow( - {}} /> - ); - - expect(element).toMatchSnapshot(); - }); - - it('should render list selection correctly', () => { - const element = shallow( - {}} /> - ); - - expect(element).toMatchSnapshot(); - }); - - it('should render singular count correctly', () => { - const element = shallow( - {}} /> - ); - - expect(element).toMatchSnapshot(); - }); - - it('should render plural count correctly', () => { - const element = shallow( - {}} /> - ); - - expect(element).toMatchSnapshot(); - }); - - it('should trigger onViewTypeChange', async () => { - const onToggle = jest.fn(); - const element = render( - - ); - - (await element.findAllByTestId('viewTypeToggleButton'))[0].click(); - - expect(onToggle).toBeCalledWith('grid'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx deleted file mode 100644 index 89e9a8997f565..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { ViewType } from '../../../state'; -import { ViewTypeToggle } from '../view_type_toggle'; - -export interface ControlPanelProps { - totalItemCount: number; - currentViewType: ViewType; - onViewTypeChange: (value: ViewType) => void; -} - -export const ControlPanel = memo( - ({ totalItemCount, currentViewType, onViewTypeChange }) => { - return ( - - - - {i18n.translate('xpack.securitySolution.trustedapps.list.totalCount', { - defaultMessage: - '{totalItemCount, plural, one {# trusted application} other {# trusted applications}}', - values: { totalItemCount }, - })} - - - - - - - ); - } -); - -ControlPanel.displayName = 'ControlPanel'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx index 43e5516750a1d..d4b02b6ac467a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/empty_state.tsx @@ -41,7 +41,7 @@ export const EmptyState = memo<{ > } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap index 7439245bc9571..cbeea78f51040 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap @@ -22,40 +22,40 @@ exports[`trusted_app_card TrustedAppCard should render correctly 1`] = ` } /> } /> } /> } /> } @@ -72,6 +72,7 @@ exports[`trusted_app_card TrustedAppCard should render correctly 1`] = ` /> @@ -146,40 +146,40 @@ exports[`trusted_app_card TrustedAppCard should trim long texts 1`] = ` } /> } /> } /> } /> } @@ -196,6 +196,7 @@ exports[`trusted_app_card TrustedAppCard should trim long texts 1`] = ` /> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx index 2016e43f53c42..419d8aaedfe03 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/index.tsx @@ -6,6 +6,7 @@ */ import React, { memo, useCallback, useMemo } from 'react'; +import { isEmpty } from 'lodash/fp'; import { EuiTableFieldDataColumnType } from '@elastic/eui'; import { @@ -140,28 +141,30 @@ export const TrustedAppCard = memo( /> } /> - - } - /> + {!isEmpty(trustedApp.description) && ( + + } + /> + )} getEntriesColumnDefinitions(), [])} items={useMemo(() => [...trustedApp.entries], [trustedApp.entries])} badge="and" + className="trustedAppsConditionsTable" responsive /> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index 7b3ae2e2b3b27..1bc2581a520ae 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -378,15 +378,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` } .c5 { - padding: 16px; + padding: 12px 24px 24px 0; +} + +.c5.c5.c5 { + margin-left: 0; +} + +.c5 .trustedAppsConditionsTable { + margin-left: 16px; } .c3.c3.c3 { width: 40%; + margin-top: 0; + margin-bottom: 8px; } .c4.c4.c4 { width: 60%; + margin-top: 0; + margin-bottom: 8px; } .c1 { @@ -432,7 +444,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList euiDescriptionList--column euiDescriptionList--compressed" >
Name
@@ -448,7 +460,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
OS
@@ -464,9 +476,9 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
- Date Created + Date created
- Created By + Created by
- Date Modified + Date modified
- Modified By + Modified by
Description
@@ -550,7 +562,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiFlexItem euiFlexItem--flexGrow1" >
-
-
-
-
- -
-
- - - -`; - -exports[`TrustedAppsList renders correctly when loaded data 1`] = ` -
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - Name - - - - - - OS - - - - - - Date Created - - - - - - Created By - - - - - - Actions - - - - - - -
-
- Name -
-
- - - trusted app 0 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 1 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 2 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 3 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 4 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 5 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 6 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 7 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 8 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 9 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 10 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 11 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 12 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 13 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 14 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 15 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 16 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 17 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 18 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 19 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-`; - -exports[`TrustedAppsList renders correctly when loading data for the first time 1`] = ` -
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - -
-
- - - Name - - - - - - OS - - - - - - Date Created - - - - - - Created By - - - - - - Actions - - - - - - -
-
- - No items found - -
-
-
-
-
-`; - -exports[`TrustedAppsList renders correctly when loading data for the second time 1`] = ` -
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - Name - - - - - - OS - - - - - - Date Created - - - - - - Created By - - - - - - Actions - - - - - - -
-
- Name -
-
- - - trusted app 0 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 1 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 2 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 3 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 4 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 5 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 6 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 7 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 8 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 9 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 10 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 11 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 12 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 13 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 14 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 15 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 16 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 17 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 18 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 19 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-`; - -exports[`TrustedAppsList renders correctly when new page and page size set (not loading yet) 1`] = ` -
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - Name - - - - - - OS - - - - - - Date Created - - - - - - Created By - - - - - - Actions - - - - - - -
-
- Name -
-
- - - trusted app 0 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 1 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 2 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 3 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 4 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 5 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 6 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 7 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 8 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 9 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 10 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 11 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 12 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 13 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 14 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 15 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 16 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 17 - - -
-
-
- OS -
-
- - - Linux - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 18 - - -
-
-
- OS -
-
- - - Windows - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
- Name -
-
- - - trusted app 19 - - -
-
-
- OS -
-
- - - Mac - - -
-
-
- Date Created -
-
- 1 minute ago -
-
-
- Created By -
-
- - - someone - - -
-
-
- - - - Remove - - -
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-`; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.stories.tsx deleted file mode 100644 index 8c7464824158e..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.stories.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Provider } from 'react-redux'; -import { ThemeProvider } from 'styled-components'; -import { storiesOf } from '@storybook/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; - -import { KibanaContextProvider } from '../../../../../../../../../../src/plugins/kibana_react/public'; - -import { - createGlobalNoMiddlewareStore, - createListFailedResourceState, - createListLoadedResourceState, - createListLoadingResourceState, - createTrustedAppsListResourceStateChangedAction, -} from '../../../test_utils'; - -import { TrustedAppsList } from '.'; - -const now = 111111; - -const renderList = (store: ReturnType) => ( - - 'MMM D, YYYY @ HH:mm:ss.SSS' } }}> - ({ eui: euiLightVars, darkMode: false })}> - - - - -); - -storiesOf('TrustedApps/TrustedAppsList', module) - .add('default', () => { - return renderList(createGlobalNoMiddlewareStore()); - }) - .add('loading', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction(createListLoadingResourceState()) - ); - - return renderList(store); - }) - .add('error', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListFailedResourceState('Intenal Server Error') - ) - ); - - return renderList(store); - }) - .add('loaded', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 10 }, now) - ) - ); - - return renderList(store); - }) - .add('loading second time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadingResourceState(createListLoadedResourceState({ pageSize: 10 }, now)) - ) - ); - - return renderList(store); - }) - .add('long texts', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 10 }, now, true) - ) - ); - - return renderList(store); - }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.test.tsx deleted file mode 100644 index 64efda2c90ed1..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.test.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; -import { Provider } from 'react-redux'; -import { ThemeProvider } from 'styled-components'; - -import { - createSampleTrustedApp, - createListFailedResourceState, - createListLoadedResourceState, - createListLoadingResourceState, - createTrustedAppsListResourceStateChangedAction, - createUserChangedUrlAction, - createGlobalNoMiddlewareStore, -} from '../../../test_utils'; - -import { TrustedAppsList } from '.'; -import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ - eui: { - euiColorLightestShade: '#ece', - }, -}); - -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => 'mockId', -})); - -const now = 111111; - -const renderList = (store: ReturnType) => { - const Wrapper: React.FC = ({ children }) => ( - - {children} - - ); - - return render(, { wrapper: Wrapper }); -}; - -describe('TrustedAppsList', () => { - it('renders correctly initially', () => { - expect(renderList(createGlobalNoMiddlewareStore()).container).toMatchSnapshot(); - }); - - it('renders correctly when loading data for the first time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction(createListLoadingResourceState()) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when failed loading data for the first time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListFailedResourceState('Intenal Server Error') - ) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when loaded data', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 20 }, now) - ) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when new page and page size set (not loading yet)', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 20 }, now) - ) - ); - store.dispatch( - createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when loading data for the second time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadingResourceState(createListLoadedResourceState({ pageSize: 20 }, now)) - ) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when failed loading data for the second time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListFailedResourceState( - 'Intenal Server Error', - createListLoadedResourceState({ pageSize: 20 }, now) - ) - ) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when item details expanded', async () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 20 }, now) - ) - ); - - const element = renderList(store); - - (await element.findAllByTestId('trustedAppsListItemExpandButton'))[0].click(); - - expect(element.container).toMatchSnapshot(); - }); - - it('triggers deletion dialog when delete action clicked', async () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 20 }, now) - ) - ); - store.dispatch = jest.fn(); - - (await renderList(store).findAllByTestId('trustedAppDeleteAction'))[0].click(); - - expect(store.dispatch).toBeCalledWith({ - type: 'trustedAppDeletionDialogStarted', - payload: { - entry: createSampleTrustedApp(0), - }, - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx deleted file mode 100644 index 5d3b8a86b7a69..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, ReactNode, useCallback, useMemo, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, RIGHT_ALIGNMENT } from '@elastic/eui'; - -import { useHistory } from 'react-router-dom'; -import { Immutable, TrustedApp } from '../../../../../../../common/endpoint/types'; - -import { - getCurrentLocation, - getListErrorMessage, - getListItems, - getListPagination, - isListLoading, -} from '../../../store/selectors'; - -import { FormattedDate } from '../../../../../../common/components/formatted_date'; -import { TextFieldValue } from '../../../../../../common/components/text_field_value'; - -import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from '../../hooks'; - -import { ACTIONS_COLUMN_TITLE, LIST_ACTIONS, OS_TITLES, PROPERTY_TITLES } from '../../translations'; -import { TrustedAppCard, TrustedAppCardProps } from '../trusted_app_card'; -import { getTrustedAppsListPath } from '../../../../../common/routing'; - -interface DetailsMap { - [K: string]: ReactNode; -} - -const ExpandedRowContent = memo>(({ trustedApp }) => { - const dispatch = useDispatch(); - const history = useHistory(); - const location = useTrustedAppsSelector(getCurrentLocation); - - const handleOnDelete = useCallback(() => { - dispatch({ - type: 'trustedAppDeletionDialogStarted', - payload: { entry: trustedApp }, - }); - }, [dispatch, trustedApp]); - - const handleOnEdit = useCallback(() => { - history.push( - getTrustedAppsListPath({ - ...location, - show: 'edit', - id: trustedApp.id, - }) - ); - }, [history, location, trustedApp.id]); - - return ( - - ); -}); -ExpandedRowContent.displayName = 'ExpandedRowContent'; - -export const TrustedAppsList = memo(() => { - const dispatch = useDispatch(); - - const [showDetailsFor, setShowDetailsFor] = useState<{ [key: string]: boolean }>({}); - - // Cast below is needed because EuiBasicTable expects listItems to be mutable - const listItems = useTrustedAppsSelector(getListItems) as TrustedApp[]; - const pagination = useTrustedAppsSelector(getListPagination); - const listError = useTrustedAppsSelector(getListErrorMessage); - const isLoading = useTrustedAppsSelector(isListLoading); - - const toggleShowDetailsFor = useCallback((trustedAppId) => { - setShowDetailsFor((prevState) => { - const newState = { ...prevState }; - if (prevState[trustedAppId]) { - delete newState[trustedAppId]; - } else { - newState[trustedAppId] = true; - } - return newState; - }); - }, []); - - const detailsMap = useMemo(() => { - return Object.keys(showDetailsFor).reduce((expandMap, trustedAppId) => { - const trustedApp = listItems.find((ta) => ta.id === trustedAppId); - - if (trustedApp) { - expandMap[trustedAppId] = ; - } - - return expandMap; - }, {}); - }, [listItems, showDetailsFor]); - - const handleTableOnChange = useTrustedAppsNavigateCallback(({ page }) => ({ - page_index: page.index, - page_size: page.size, - })); - - const tableColumns: Array>> = useMemo(() => { - return [ - { - field: 'name', - name: PROPERTY_TITLES.name, - 'data-test-subj': 'trustedAppNameTableCell', - render(value: TrustedApp['name']) { - return ( - - ); - }, - }, - { - field: 'os', - name: PROPERTY_TITLES.os, - render(value: TrustedApp['os']) { - return ( - - ); - }, - }, - { - field: 'created_at', - name: PROPERTY_TITLES.created_at, - render(value: TrustedApp['created_at']) { - return ( - - ); - }, - }, - { - field: 'created_by', - name: PROPERTY_TITLES.created_by, - render(value: TrustedApp['created_by']) { - return ( - - ); - }, - }, - { - name: ACTIONS_COLUMN_TITLE, - actions: [ - { - name: LIST_ACTIONS.delete.name, - description: LIST_ACTIONS.delete.description, - 'data-test-subj': 'trustedAppDeleteAction', - isPrimary: true, - icon: 'trash', - color: 'danger', - type: 'icon', - onClick: (item: Immutable) => { - dispatch({ - type: 'trustedAppDeletionDialogStarted', - payload: { entry: item }, - }); - }, - }, - ], - }, - { - align: RIGHT_ALIGNMENT, - width: '40px', - isExpander: true, - render({ id }: Immutable) { - return ( - toggleShowDetailsFor(id)} - aria-label={detailsMap[id] ? 'Collapse' : 'Expand'} - iconType={detailsMap[id] ? 'arrowUp' : 'arrowDown'} - data-test-subj="trustedAppsListItemExpandButton" - /> - ); - }, - }, - ]; - }, [detailsMap, dispatch, toggleShowDetailsFor]); - - return ( - - ); -}); - -TrustedAppsList.displayName = 'TrustedAppsList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index 803e292c58eb5..9e2cad93fc51f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -19,7 +19,7 @@ export { OS_TITLES } from '../../../common/translations'; export const ABOUT_TRUSTED_APPS = i18n.translate('xpack.securitySolution.trustedapps.aboutInfo', { defaultMessage: 'Add a trusted application to improve performance or alleviate conflicts with other applications running on ' + - 'your hosts. Trusted applications are applied to hosts running the Endpoint Security integration on their agents.', + 'your hosts.', }); export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { @@ -71,16 +71,16 @@ export const PROPERTY_TITLES: Readonly< defaultMessage: 'OS', }), created_at: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.createdAt', { - defaultMessage: 'Date Created', + defaultMessage: 'Date created', }), created_by: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.createdBy', { - defaultMessage: 'Created By', + defaultMessage: 'Created by', }), updated_at: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.updatedAt', { - defaultMessage: 'Date Modified', + defaultMessage: 'Date modified', }), updated_by: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.updatedBy', { - defaultMessage: 'Modified By', + defaultMessage: 'Modified by', }), description: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.description', { defaultMessage: 'Description', diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx index 94fd1a2bb4991..a86a08a894ed9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx @@ -48,23 +48,33 @@ const getDeletionSuccessMessage = (entry: Immutable) => { }; const getCreationSuccessMessage = (entry: Immutable) => { - return i18n.translate( - 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle', - { - defaultMessage: '"{name}" has been added to the Trusted Applications list.', - values: { name: entry.name }, - } - ); + return { + title: i18n.translate('xpack.securitySolution.trustedapps.creationSuccess.title', { + defaultMessage: 'Success!', + }), + text: i18n.translate( + 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle', + { + defaultMessage: '"{name}" has been added to the Trusted Applications list.', + values: { name: entry.name }, + } + ), + }; }; const getUpdateSuccessMessage = (entry: Immutable) => { - return i18n.translate( - 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.updateSuccessToastTitle', - { - defaultMessage: '"{name}" has been updated successfully', - values: { name: entry.name }, - } - ); + return { + title: i18n.translate('xpack.securitySolution.trustedapps.updateSuccess.title', { + defaultMessage: 'Success!', + }), + text: i18n.translate( + 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.updateSuccessToastTitle', + { + defaultMessage: '"{name}" has been updated.', + values: { name: entry.name }, + } + ), + }; }; export const TrustedAppsNotifications = memo(() => { 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 2ba357a349b5d..ff7ba8068b4ff 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 @@ -46,8 +46,7 @@ const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as j describe('When on the Trusted Apps Page', () => { const expectedAboutInfo = 'Add a trusted application to improve performance or alleviate conflicts with other ' + - 'applications running on your hosts. Trusted applications are applied to hosts running the Endpoint Security ' + - 'integration on their agents.'; + 'applications running on your hosts.'; const generator = new EndpointDocGenerator('policy-list'); @@ -170,7 +169,7 @@ describe('When on the Trusted Apps Page', () => { it('should display a Add Trusted App button', async () => { const { getByTestId } = await renderWithListData(); const addButton = getByTestId('trustedAppsListAddButton'); - expect(addButton.textContent).toBe('Add Trusted Application'); + expect(addButton.textContent).toBe('Add trusted application'); }); it('should display the searchExceptions', async () => { @@ -355,73 +354,6 @@ describe('When on the Trusted Apps Page', () => { }); }); }); - - describe('and the List view is being displayed', () => { - let renderResult: ReturnType; - - const expandFirstRow = () => { - reactTestingLibrary.act(() => { - fireEvent.click(renderResult.getByTestId('trustedAppsListItemExpandButton')); - }); - }; - - beforeEach(async () => { - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps?view_type=list'); - }); - - renderResult = await renderWithListData(); - }); - - it('should display the list', () => { - expect(renderResult.getByTestId('trustedAppsList')); - }); - - it('should show a card when row is expanded', () => { - expandFirstRow(); - expect(renderResult.getByTestId('trustedAppCard')); - }); - - it('should show Edit flyout when edit button on card is clicked', () => { - expandFirstRow(); - reactTestingLibrary.act(() => { - fireEvent.click(renderResult.getByTestId('trustedAppEditButton')); - }); - expect(renderResult.findByTestId('addTrustedAppFlyout')); - }); - - it('should reflect updated information on row and card when updated data is received', async () => { - expandFirstRow(); - reactTestingLibrary.act(() => { - const updatedListContent = createListApiResponse(); - updatedListContent.data[0]!.name = 'updated trusted app'; - updatedListContent.data[0]!.description = 'updated trusted app description'; - - mockedContext.store.dispatch({ - type: 'trustedAppsListResourceStateChanged', - payload: { - newState: { - type: 'LoadedResourceState', - data: { - items: updatedListContent.data, - pageIndex: updatedListContent.page, - pageSize: updatedListContent.per_page, - totalItemsCount: updatedListContent.total, - timestamp: Date.now(), - }, - }, - }, - }); - }); - - // The additional prefix of `Name` is due to the hidden element in DOM that is only shown - // for mobile devices (inserted by the EuiBasicTable) - expect(renderResult.getByTestId('trustedAppNameTableCell').textContent).toEqual( - 'Nameupdated trusted app' - ); - expect(renderResult.getByText('updated trusted app description')); - }); - }); }); describe('and the Add Trusted App button is clicked', () => { @@ -638,9 +570,10 @@ describe('When on the Trusted Apps Page', () => { }); it('should show success toast notification', () => { - expect(coreStart.notifications.toasts.addSuccess.mock.calls[0][0]).toEqual( - '"one app" has been added to the Trusted Applications list.' - ); + expect(coreStart.notifications.toasts.addSuccess.mock.calls[0][0]).toEqual({ + text: '"one app" has been added to the Trusted Applications list.', + title: 'Success!', + }); }); it('should trigger the List to reload', () => { @@ -925,18 +858,7 @@ describe('When on the Trusted Apps Page', () => { describe('and the back button is present', () => { let renderResult: ReturnType; beforeEach(async () => { - renderResult = render(); - await act(async () => { - await waitForAction('trustedAppsListResourceStateChanged'); - }); - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps', { - onBackButtonNavigateTo: [{ appId: 'appId' }], - backButtonLabel: 'back to fleet', - backButtonUrl: '/fleet', - }); - }); - + // Ensure implementation is defined before render to avoid undefined responses from hidden api calls const priorMockImplementation = coreStart.http.get.getMockImplementation(); // @ts-ignore coreStart.http.get.mockImplementation((path, options) => { @@ -957,6 +879,18 @@ describe('When on the Trusted Apps Page', () => { return priorMockImplementation(path); } }); + + renderResult = render(); + await act(async () => { + await waitForAction('trustedAppsListResourceStateChanged'); + }); + reactTestingLibrary.act(() => { + history.push('/administration/trusted_apps', { + onBackButtonNavigateTo: [{ appId: 'appId' }], + backButtonLabel: 'back to fleet', + backButtonUrl: '/fleet', + }); + }); }); it('back button is present', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index a7e86a6703bb5..70698aec509ba 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -15,12 +15,12 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, EuiLoadingSpinner, EuiSpacer, + EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { ViewType } from '../state'; import { checkingIfEntriesExist, entriesExist, @@ -32,9 +32,7 @@ import { import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from './hooks'; import { AdministrationListPage } from '../../../components/administration_list_page'; import { CreateTrustedAppFlyout } from './components/create_trusted_app_flyout'; -import { ControlPanel } from './components/control_panel'; import { TrustedAppsGrid } from './components/trusted_apps_grid'; -import { TrustedAppsList } from './components/trusted_apps_list'; import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; import { TrustedAppsNotifications } from './trusted_apps_notifications'; import { AppAction } from '../../../../common/store/actions'; @@ -73,9 +71,6 @@ export const TrustedAppsPage = memo(() => { show: undefined, id: undefined, })); - const handleViewTypeChange = useTrustedAppsNavigateCallback((viewType: ViewType) => ({ - view_type: viewType, - })); const handleOnSearch = useCallback( (query: string, includedPolicies?: string, excludedPolicies?: string) => { @@ -109,7 +104,7 @@ export const TrustedAppsPage = memo(() => { > ); @@ -144,19 +139,18 @@ export const TrustedAppsPage = memo(() => { > - - - + + {i18n.translate('xpack.securitySolution.trustedapps.list.totalCount', { + defaultMessage: + 'Showing {totalItemsCount, plural, one {# trusted application} other {# trusted applications}}', + values: { totalItemsCount }, + })} + + + - - - {location.view_type === 'grid' && } - {location.view_type === 'list' && } + @@ -172,7 +166,7 @@ export const TrustedAppsPage = memo(() => { title={ } headerBackComponent={backButton} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx index 2b3cec1111be9..553281144ebff 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx @@ -65,6 +65,7 @@ export const filterNetworkData: Filter[] = [ export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( { setDismissMessage(true); addMessage('management', 'dismissEndpointNotice'); }, [addMessage]); - const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; + const { + endpointPrivileges: { canAccessFleet }, + alertsPrivileges, + } = useUserPrivileges(); const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); return ( <> @@ -95,23 +98,27 @@ const OverviewComponent = () => { - - - - - - - - + {alertsPrivileges?.read && ( + <> + + + + + + + + + + )} = ({ timelineId, timelineStatus, @@ -100,7 +106,7 @@ const ActiveTimelinesComponent: React.FC = ({ /> - {title} + {title} {!isOpen && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index ee994e2a16f46..e3a1152428d62 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -12,9 +12,10 @@ import { EuiToolTip, EuiButtonIcon, EuiText, + EuiButtonEmpty, EuiTextColor, } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { MouseEventHandler, MouseEvent, useCallback, useMemo } from 'react'; import { isEmpty, get, pick } from 'lodash/fp'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; @@ -52,7 +53,9 @@ import * as i18n from './translations'; import * as commonI18n from '../../timeline/properties/translations'; import { getTimelineStatusByIdSelector } from './selectors'; import { TimelineKPIs } from './kpis'; -import { LineClamp } from '../../../../common/components/line_clamp'; + +import { setActiveTabTimeline } from '../../../store/timeline/actions'; +import { useIsOverflow } from '../../../../common/hooks/use_is_overflow'; // to hide side borders const StyledPanel = styled(EuiPanel)` @@ -67,6 +70,10 @@ interface FlyoutHeaderPanelProps { timelineId: string; } +const ActiveTimelinesContainer = styled(EuiFlexItem)` + overflow: hidden; +`; + const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); const { indexPattern, browserFields } = useSourcererScope(SourcererScopeName.timeline); @@ -145,7 +152,7 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline > - + = ({ timeline isOpen={show} updated={updated} /> - + {show && ( @@ -190,6 +197,34 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline export const FlyoutHeaderPanel = React.memo(FlyoutHeaderPanelComponent); +const StyledDiv = styled.div` + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-word; +`; + +const ReadMoreButton = ({ + description, + onclick, +}: { + description: string; + onclick: MouseEventHandler; +}) => { + const [isOverflow, ref] = useIsOverflow(description); + return ( + <> + {description} + {isOverflow && ( + + {i18n.READ_MORE} + + )} + + ); +}; + const StyledTimelineHeader = styled(EuiFlexGroup)` ${({ theme }) => `margin: ${theme.eui.euiSizeXS} ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS};`} flex: 0; @@ -197,6 +232,7 @@ const StyledTimelineHeader = styled(EuiFlexGroup)` const TimelineStatusInfoContainer = styled.span` ${({ theme }) => `margin-left: ${theme.eui.euiSizeS};`} + white-space: nowrap; `; const KpisContainer = styled.div` @@ -208,6 +244,14 @@ const RowFlexItem = styled(EuiFlexItem)` align-items: center; `; +const TimelineTitleContainer = styled.h3` + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + word-break: break-word; +`; + const TimelineNameComponent: React.FC = ({ timelineId }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { title, timelineType } = useDeepEqualSelector((state) => @@ -224,9 +268,11 @@ const TimelineNameComponent: React.FC = ({ timelineId }) => { const content = useMemo(() => title || placeholder, [title, placeholder]); return ( - -

{content}

-
+ + + {content} + + ); }; @@ -237,15 +283,24 @@ const TimelineDescriptionComponent: React.FC = ({ timelineId const description = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).description ); + const dispatch = useDispatch(); + + const onReadMore: MouseEventHandler = useCallback( + (event: MouseEvent) => { + dispatch( + setActiveTabTimeline({ + id: timelineId, + activeTab: TimelineTabs.notes, + scrollToTop: true, + }) + ); + }, + [dispatch, timelineId] + ); + return ( - {description ? ( - - {description} - - ) : ( - commonI18n.DESCRIPTION - )} + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts index 7483d0cae71c5..2f0717dea32aa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts @@ -61,6 +61,10 @@ export const USER_KPI_TITLE = i18n.translate('xpack.securitySolution.timeline.kp defaultMessage: 'Users', }); +export const READ_MORE = i18n.translate('xpack.securitySolution.timeline.properties.readMore', { + defaultMessage: 'Read More', +}); + export const TIMELINE_TOGGLE_BUTTON_ARIA_LABEL = ({ isOpen, title, diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap index 69e06bc7e0d1b..32e17a19045b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap @@ -6,6 +6,7 @@ exports[`NewNote renders correctly 1`] = ` > void; updateNewNote: UpdateInternalNewNote; -}>(({ associateNote, newNote, onCancelAddNote, updateNewNote }) => { + autoFocusDisabled?: boolean; +}>(({ associateNote, newNote, onCancelAddNote, updateNewNote, autoFocusDisabled = false }) => { const dispatch = useDispatch(); const updateNote = useCallback((note: Note) => dispatch(appActions.updateNote({ note })), [ @@ -87,7 +88,12 @@ export const AddNote = React.memo<{

{i18n.YOU_ARE_EDITING_A_NOTE}

- + {onCancelAddNote != null ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx index 761df470e6f4d..bf1a2227f6f99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/new_note.tsx @@ -24,7 +24,8 @@ export const NewNote = React.memo<{ noteInputHeight: number; note: string; updateNewNote: UpdateInternalNewNote; -}>(({ note, noteInputHeight, updateNewNote }) => { + autoFocusDisabled?: boolean; +}>(({ note, noteInputHeight, updateNewNote, autoFocusDisabled = false }) => { return ( ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx index 0c611ca5106e8..1cca5a3999b81 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx @@ -10,11 +10,21 @@ import moment from 'moment'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; import '../../../../common/mock/formatted_relative'; - +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult, TimelineResultNote } from '../types'; import { NotePreviews } from '.'; +jest.mock('../../../../common/hooks/use_selector'); + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => jest.fn(), + }; +}); + describe('NotePreviews', () => { let mockResults: OpenTimelineResult[]; let note1updated: number; @@ -26,6 +36,7 @@ describe('NotePreviews', () => { note1updated = moment('2019-03-24T04:12:33.000Z').valueOf(); note2updated = moment(note1updated).add(1, 'minute').valueOf(); note3updated = moment(note2updated).add(1, 'minute').valueOf(); + (useDeepEqualSelector as jest.Mock).mockReset(); }); test('it renders a note preview for each note when isModal is false', () => { @@ -48,24 +59,6 @@ describe('NotePreviews', () => { }); }); - test('it does NOT render the preview container if notes is undefined', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); - }); - - test('it does NOT render the preview container if notes is null', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); - }); - - test('it does NOT render the preview container if notes is empty', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); - }); - test('it filters-out non-unique savedObjectIds', () => { const nonUniqueNotes: TimelineResultNote[] = [ { @@ -145,4 +138,26 @@ describe('NotePreviews', () => { expect(wrapper.find(`.euiCommentEvent__headerUsername`).at(2).text()).toEqual('bob'); }); + + test('it renders timeline description as a note when showTimelineDescription is true and timelineId is defined', () => { + const timeline = mockTimelineResults[0]; + (useDeepEqualSelector as jest.Mock).mockReturnValue(timeline); + + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="note-preview-description"]').first().text()).toContain( + timeline.description + ); + }); + + test('it does`t render timeline description as a note when it is undefined', () => { + const timeline = mockTimelineResults[0]; + (useDeepEqualSelector as jest.Mock).mockReturnValue({ ...timeline, description: undefined }); + + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="note-preview-description"]').exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index 5581ea4e5c165..aff12b74fbfbf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -6,7 +6,13 @@ */ import { uniqBy } from 'lodash/fp'; -import { EuiAvatar, EuiButtonIcon, EuiCommentList, EuiScreenReaderOnly } from '@elastic/eui'; +import { + EuiAvatar, + EuiButtonIcon, + EuiCommentList, + EuiScreenReaderOnly, + EuiText, +} from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; @@ -15,12 +21,13 @@ import { useDispatch } from 'react-redux'; import { TimelineResultNote } from '../types'; import { getEmptyValue, defaultToEmptyTag } from '../../../../common/components/empty_value'; import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; -import { timelineActions } from '../../../store/timeline'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { NOTE_CONTENT_CLASS_NAME } from '../../timeline/body/helpers'; import * as i18n from './translations'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { sourcererSelectors } from '../../../../common/store'; +import { SaveTimelineButton } from '../../timeline/header/save_timeline_button'; export const NotePreviewsContainer = styled.section` padding-top: ${({ theme }) => `${theme.eui.euiSizeS}`}; @@ -78,10 +85,45 @@ interface NotePreviewsProps { eventIdToNoteIds?: Record; notes?: TimelineResultNote[] | null; timelineId?: string; + showTimelineDescription?: boolean; } export const NotePreviews = React.memo( - ({ eventIdToNoteIds, notes, timelineId }) => { + ({ eventIdToNoteIds, notes, timelineId, showTimelineDescription }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const timeline = useDeepEqualSelector((state) => + timelineId ? getTimeline(state, timelineId) : null + ); + + const descriptionList = useMemo( + () => + showTimelineDescription && timelineId && timeline?.description + ? [ + { + username: defaultToEmptyTag(timeline.updatedBy), + event: i18n.ADDED_A_DESCRIPTION, + 'data-test-subj': 'note-preview-description', + id: 'note-preview-description', + timestamp: timeline.updated ? ( + + ) : ( + getEmptyValue() + ), + children: {timeline.description}, + timelineIcon: ( + + ), + actions: , + }, + ] + : [], + [timeline, timelineId, showTimelineDescription] + ); + const notesList = useMemo( () => uniqBy('savedObjectId', notes).map((note) => { @@ -125,11 +167,12 @@ export const NotePreviews = React.memo( [eventIdToNoteIds, notes, timelineId] ); - if (notes == null || notes.length === 0) { - return null; - } - - return ; + return ( + + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts index 0945050a34a4d..c2d01704c2d9e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts @@ -18,6 +18,13 @@ export const ADDED_A_NOTE = i18n.translate('xpack.securitySolution.timeline.adde defaultMessage: 'added a note', }); +export const ADDED_A_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.timeline.addedADescriptionLabel', + { + defaultMessage: 'added description', + } +); + export const AN_UNKNOWN_USER = i18n.translate( 'xpack.securitySolution.timeline.anUnknownUserLabel', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx index 1826413110f1e..bdb55aaf20969 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx @@ -27,6 +27,15 @@ const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); jest.mock('../../../../common/lib/kibana'); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => jest.fn(), + useSelector: () => jest.fn(), + }; +}); + describe('#getCommonColumns', () => { let mockResults: OpenTimelineResult[]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx index 65963c9609320..21262d66fdbfe 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.tsx @@ -20,7 +20,7 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; import { TimelineType } from '../../../../../common/types/timeline'; -const DescriptionCell = styled.span` +const LineClampTextContainer = styled.span` text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 5; @@ -79,7 +79,11 @@ export const getCommonColumns = ({ }) } > - {isUntitled(timelineResult) ? i18n.UNTITLED_TIMELINE : title} + {isUntitled(timelineResult) ? ( + i18n.UNTITLED_TIMELINE + ) : ( + {title} + )} ) : (
@@ -93,9 +97,9 @@ export const getCommonColumns = ({ field: 'description', name: i18n.DESCRIPTION, render: (description: string) => ( - + {description != null && description.trim().length > 0 ? description : getEmptyTagValue()} - + ), sortable: false, }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index ba53ab87b4633..9db97bc22fb18 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -68,11 +68,13 @@ interface EventDetailsPanelProps { timelineId: string; } +const SECURITY_SOLUTION_ALERT_CONSUMERS: AlertConsumers[] = [AlertConsumers.SIEM]; + const EventDetailsPanelComponent: React.FC = ({ - alertConsumers, + alertConsumers = SECURITY_SOLUTION_ALERT_CONSUMERS, // Default to Security Solution so only other applications have to pass this in browserFields, docValueFields, - entityType, + entityType = 'events', // Default to events so only alerts have to pass entityType in expandedEvent, handleOnEventClosed, isFlyoutView, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx index 9a6a2f6c95567..ff51f61a9a2b8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx @@ -23,6 +23,14 @@ import { FlowTarget } from '../../../../common/search_strategy/security_solution jest.mock('../../../common/lib/kibana'); +jest.mock('@kbn/alerts', () => ({ + useGetUserAlertsPermissions: () => ({ + loading: false, + crud: true, + read: true, + }), +})); + describe('Details Panel Component', () => { const state: State = { ...mockGlobalState }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index f70ebfc31e2c7..b982c2240ac7c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -21,12 +21,23 @@ jest.mock('../../../../../common/hooks/use_selector', () => ({ useShallowEqualSelector: jest.fn(), })); +jest.mock('@kbn/alerts', () => ({ + useGetUserAlertsPermissions: () => ({ + loading: false, + crud: true, + read: true, + }), +})); + jest.mock('../../../../../common/lib/kibana', () => ({ useKibana: () => ({ services: { application: { navigateToApp: jest.fn(), getUrlForApp: jest.fn(), + capabilities: { + siem: { crud_alerts: true, read_alerts: true }, + }, }, uiSettings: { get: jest.fn(), @@ -74,6 +85,8 @@ describe('Actions', () => { timelineId={'test'} refetch={jest.fn()} showCheckboxes={true} + setEventsLoading={jest.fn()} + setEventsDeleted={jest.fn()} /> ); @@ -104,6 +117,8 @@ describe('Actions', () => { onEventDetailsPanelOpened={jest.fn()} onRowSelected={jest.fn()} showCheckboxes={false} + setEventsLoading={jest.fn()} + setEventsDeleted={jest.fn()} /> ); @@ -136,6 +151,8 @@ describe('Actions', () => { onEventDetailsPanelOpened={jest.fn()} onRowSelected={jest.fn()} showCheckboxes={true} + setEventsLoading={jest.fn()} + setEventsDeleted={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx index ecacbc51e395a..789cd5211f121 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx @@ -47,6 +47,8 @@ describe('Columns', () => { eventIdToNoteIds={{}} leadingControlColumns={[defaultControlColumn]} trailingControlColumns={[]} + setEventsLoading={jest.fn()} + setEventsDeleted={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index 11bf88977fe61..82207906a6295 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -12,6 +12,7 @@ import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-g import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import type { SetEventsLoading, SetEventsDeleted } from '../../../../../../../timelines/common'; import { ColumnHeaderOptions, CellValueElementProps, @@ -76,6 +77,8 @@ interface DataDrivenColumnProps { toggleShowNotes: () => void; trailingControlColumns: ControlColumnProps[]; leadingControlColumns: ControlColumnProps[]; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; } const SPACE = ' '; @@ -149,6 +152,8 @@ const TgridActionTdCell = ({ tabType, timelineId, toggleShowNotes, + setEventsLoading, + setEventsDeleted, }: ActionProps & { columnId: string; hasRowRenderers: boolean; @@ -200,6 +205,8 @@ const TgridActionTdCell = ({ showNotes={showNotes} timelineId={timelineId} toggleShowNotes={toggleShowNotes} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> )} @@ -302,6 +309,8 @@ export const DataDrivenColumns = React.memo( toggleShowNotes, trailingControlColumns, leadingControlColumns, + setEventsLoading, + setEventsDeleted, }) => { const trailingActionCells = useMemo( () => @@ -348,6 +357,8 @@ export const DataDrivenColumns = React.memo( tabType={tabType} timelineId={timelineId} toggleShowNotes={toggleShowNotes} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> ) ); @@ -378,6 +389,8 @@ export const DataDrivenColumns = React.memo( timelineId, toggleShowNotes, trailingActionCells, + setEventsLoading, + setEventsDeleted, ] ); const ColumnHeaders = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index 60fd170a47532..d20c62348f07f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -24,11 +24,23 @@ import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin' jest.mock('../../../../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; +jest.mock('@kbn/alerts', () => ({ + useGetUserAlertsPermissions: () => ({ + loading: false, + crud: true, + read: true, + }), +})); jest.mock('../../../../../common/hooks/use_selector'); jest.mock('../../../../../common/lib/kibana', () => ({ useKibana: () => ({ services: { timelines: { ...mockTimelines }, + application: { + capabilities: { + siem: { crud_alerts: true, read_alerts: true }, + }, + }, }, }), useToasts: jest.fn().mockReturnValue({ @@ -89,6 +101,8 @@ describe('EventColumnView', () => { isEventPinned: false, leadingControlColumns: [defaultControlColumn], trailingControlColumns: [], + setEventsLoading: jest.fn(), + setEventsDeleted: jest.fn(), }; test('it does NOT render a notes button when isEventsViewer is true', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 298ce252ba925..3876b91c8bdaa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -9,6 +9,7 @@ import React, { useMemo } from 'react'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import type { SetEventsLoading, SetEventsDeleted } from '../../../../../../../timelines/common'; import { OnRowSelected } from '../../events'; import { EventsTrData, EventsTdGroupActions } from '../../styles'; import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns'; @@ -47,6 +48,8 @@ interface Props { toggleShowNotes: () => void; leadingControlColumns: ControlColumnProps[]; trailingControlColumns: ControlColumnProps[]; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; } export const EventColumnView = React.memo( @@ -76,6 +79,8 @@ export const EventColumnView = React.memo( toggleShowNotes, leadingControlColumns, trailingControlColumns, + setEventsLoading, + setEventsDeleted, }) => { // Each action button shall announce itself to screen readers via an `aria-label` // in the following format: @@ -139,6 +144,8 @@ export const EventColumnView = React.memo( tabType={tabType} timelineId={timelineId} toggleShowNotes={toggleShowNotes} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> )} @@ -167,6 +174,8 @@ export const EventColumnView = React.memo( tabType, timelineId, toggleShowNotes, + setEventsLoading, + setEventsDeleted, ] ); return ( @@ -201,6 +210,8 @@ export const EventColumnView = React.memo( selectedEventIds={selectedEventIds} showNotes={showNotes} toggleShowNotes={toggleShowNotes} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 302aead337ed7..bcfdf83eae90b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -18,6 +18,7 @@ import { TimelineId, TimelineTabs, } from '../../../../../../common/types/timeline'; +import type { SetEventsDeleted, SetEventsLoading } from '../../../../../../../timelines/common'; import { BrowserFields } from '../../../../../common/containers/source'; import { TimelineItem, @@ -212,6 +213,20 @@ const StatefulEventComponent: React.FC = ({ [dispatch, event, isEventPinned, timelineId] ); + const setEventsLoading = useCallback( + ({ eventIds, isLoading }) => { + dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading })); + }, + [dispatch, timelineId] + ); + + const setEventsDeleted = useCallback( + ({ eventIds, isDeleted }) => { + dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted })); + }, + [dispatch, timelineId] + ); + const RowRendererContent = useMemo( () => ( @@ -276,6 +291,8 @@ const StatefulEventComponent: React.FC = ({ toggleShowNotes={onToggleShowNotes} leadingControlColumns={leadingControlColumns} trailingControlColumns={trailingControlColumns} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 3f805a21afa6b..1bfefbd1197a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -37,6 +37,9 @@ jest.mock('../../../../common/lib/kibana', () => { application: { navigateToApp: jest.fn(), getUrlForApp: jest.fn(), + capabilities: { + siem: { crud_alerts: true, read_alerts: true }, + }, }, uiSettings: { get: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 06ed901110962..5eec4ef66f39c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -202,11 +202,11 @@ const FormattedFieldValueComponent: React.FC<{ } > - <>{value} + {value} ) : ( - <>{value} + {value} ); } else { const contentValue = getOrEmptyTagFromValue(value); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx index c7e15dae0035e..3343882e0071a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx @@ -52,7 +52,11 @@ export const RenderRuleName: React.FC = ({ const ruleId = linkValue; const { search } = useFormatUrl(SecurityPageName.rules); const { navigateToApp, getUrlForApp } = useKibana().services.application; - const content = truncate ? {value} : value; + const content = truncate ? ( + {value} + ) : ( + value + ); const goToRuleDetails = useCallback( (ev) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx index 09248b832490a..d75bf436028f5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx @@ -14,7 +14,7 @@ import { DefaultDraggable } from '../../../../../common/components/draggables'; const mapping = { open: 'primary', - 'in-progress': 'warning', + acknowledged: 'warning', closed: 'default', }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index 28b795378d249..b67b9348f51aa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -13,7 +13,6 @@ import { EuiFlyoutFooter, EuiBadge, } from '@elastic/eui'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useCallback } from 'react'; import styled from 'styled-components'; @@ -152,8 +151,6 @@ export type Props = OwnProps & PropsFromRedux; const NO_SORTING: Sort[] = []; -const alertConsumers: AlertConsumers[] = [AlertConsumers.SIEM]; - export const EqlTabContentComponent: React.FC = ({ activeTab, columns, @@ -349,7 +346,6 @@ export const EqlTabContentComponent: React.FC = ({ = ({ const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { selectedPatterns } = useSourcererScope(SourcererScopeName.timeline); - const { graphEventId, savedObjectId, timelineType } = useDeepEqualSelector((state) => + const { graphEventId, savedObjectId, timelineType, description } = useDeepEqualSelector((state) => pick( - ['graphEventId', 'savedObjectId', 'timelineType'], + ['graphEventId', 'savedObjectId', 'timelineType', 'description'], getTimeline(state, timelineId) ?? timelineDefaults ) ); @@ -146,6 +146,7 @@ const StatefulTimelineComponent: React.FC = ({ setTimelineFullScreen={setTimelineFullScreen} timelineId={timelineId} timelineType={timelineType} + timelineDescription={description} timelineFullScreen={timelineFullScreen} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx index 2853a5afccdd2..e9b1e8046cec0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx @@ -16,7 +16,6 @@ import { EuiPanel, EuiHorizontalRule, } from '@elastic/eui'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import React, { Fragment, useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -25,7 +24,10 @@ import styled from 'styled-components'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineActions } from '../../../store/timeline'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { + useDeepEqualSelector, + useShallowEqualSelector, +} from '../../../../common/hooks/use_selector'; import { TimelineStatus, TimelineTabs } from '../../../../../common/types/timeline'; import { appSelectors } from '../../../../common/store/app'; import { AddNote } from '../../notes/add_note'; @@ -35,6 +37,8 @@ import { NotePreviews } from '../../open_timeline/note_previews'; import { TimelineResultNote } from '../../open_timeline/types'; import { getTimelineNoteSelector } from './selectors'; import { DetailsPanel } from '../../side_panel'; +import { getScrollToTopSelector } from '../tabs_content/selectors'; +import { useScrollToTop } from '../../../../common/components/scroll_to_top'; const FullWidthFlexGroup = styled(EuiFlexGroup)` width: 100%; @@ -66,8 +70,6 @@ const Username = styled(EuiText)` font-weight: bold; `; -const alertConsumers: AlertConsumers[] = [AlertConsumers.SIEM]; - interface UsernameWithAvatar { username: string; } @@ -126,6 +128,12 @@ interface NotesTabContentProps { const NotesTabContentComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); + + const getScrollToTop = useMemo(() => getScrollToTopSelector(), []); + const scrollToTop = useShallowEqualSelector((state) => getScrollToTop(state, timelineId)); + + useScrollToTop('#scrollableNotes', !!scrollToTop); + const getTimelineNotes = useMemo(() => getTimelineNoteSelector(), []); const { createdBy, @@ -174,7 +182,6 @@ const NotesTabContentComponent: React.FC = ({ timelineId } () => expandedDetail[TimelineTabs.notes]?.panelView ? ( = ({ timelineId } return ( - +

{NOTES}

- + {!isImmutable && ( - + )}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index 685f6c77c3fab..b5e3d853bc81c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -6,7 +6,6 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import { isEmpty } from 'lodash/fp'; import React, { useMemo, useCallback } from 'react'; import styled from 'styled-components'; @@ -89,8 +88,6 @@ const VerticalRule = styled.div` VerticalRule.displayName = 'VerticalRule'; -const alertConsumers: AlertConsumers[] = [AlertConsumers.SIEM]; - interface OwnProps { renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; @@ -269,7 +266,6 @@ export const PinnedTabContentComponent: React.FC = ({ theme.eui.paddingSizes.s}; `; -const alertConsumers: AlertConsumers[] = [AlertConsumers.SIEM]; - const isTimerangeSame = (prevProps: Props, nextProps: Props) => prevProps.end === nextProps.end && prevProps.start === nextProps.start && @@ -417,7 +414,6 @@ export const QueryTabContentComponent: React.FC = ({ = ({ timelineFullScreen, timelineType, graphEventId, + timelineDescription, }) => { const dispatch = useDispatch(); const getActiveTab = useMemo(() => getActiveTabSelector(), []); @@ -233,6 +236,7 @@ const TabsContentComponent: React.FC = ({ const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId)); const showTimeline = useShallowEqualSelector((state) => getShowTimeline(state, timelineId)); + const numberOfPinnedEvents = useShallowEqualSelector((state) => getNumberOfPinnedEvents(state, timelineId) ); @@ -253,8 +257,10 @@ const TabsContentComponent: React.FC = ({ }, [globalTimelineNoteIds, eventIdToNoteIds]); const numberOfNotes = useMemo( - () => appNotes.filter((appNote) => allTimelineNoteIds.includes(appNote.id)).length, - [appNotes, allTimelineNoteIds] + () => + appNotes.filter((appNote) => allTimelineNoteIds.includes(appNote.id)).length + + (isEmpty(timelineDescription) ? 0 : 1), + [appNotes, allTimelineNoteIds, timelineDescription] ); const setQueryAsActiveTab = useCallback(() => { @@ -362,6 +368,7 @@ const TabsContentComponent: React.FC = ({ rowRenderers={rowRenderers} timelineId={timelineId} timelineType={timelineType} + timelineDescription={timelineDescription} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts index ccb07135747f5..04045e94aee25 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/selectors.ts @@ -27,3 +27,6 @@ export const getEventIdToNoteIdsSelector = () => export const getNotesSelector = () => createSelector(selectNotesById, (notesById) => Object.values(notesById)); + +export const getScrollToTopSelector = () => + createSelector(selectTimeline, (timeline) => timeline?.scrollToTop); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index d5f692cc9dc17..d0d5fdacad312 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -17,7 +17,7 @@ import { import { KqlMode, TimelineModel } from './model'; import { InsertTimeline } from './types'; import { FieldsEqlOptions } from '../../../../common/search_strategy/timeline'; -import { +import type { TimelineEventsType, RowRendererId, TimelineTabs, @@ -204,6 +204,7 @@ export const updateIndexNames = actionCreator<{ export const setActiveTabTimeline = actionCreator<{ id: string; activeTab: TimelineTabs; + scrollToTop?: boolean; }>('SET_ACTIVE_TAB_TIMELINE'); export const toggleModalSaveTimeline = actionCreator<{ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index d8fd82005dfbe..f411c6ffac9b7 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -19,6 +19,7 @@ export const timelineDefaults: SubsetTimelineModel & activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.query, columns: defaultHeaders, + defaultColumns: defaultHeaders, dataProviders: [], dateRange: { start, end }, deletedEventIds: [], diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts index eabcdd53fb994..8b40febbfe993 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts @@ -14,51 +14,53 @@ import { TimelineModel } from './model'; describe('Epic Timeline', () => { describe('#convertTimelineAsInput ', () => { test('should return a TimelineInput instead of TimelineModel ', () => { + const columns: TimelineModel['columns'] = [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + initialWidth: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + initialWidth: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + initialWidth: 180, + }, + ]; const timelineModel: TimelineModel = { activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.notes, - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - initialWidth: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - initialWidth: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - initialWidth: 180, - }, - ], + columns, + defaultColumns: columns, dataProviders: [ { id: 'hosts-table-hostName-DESKTOP-QBBSCUT', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index ef47b474350c7..7a16b62cd45e6 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -11,6 +11,7 @@ import type { TimelineType, TimelineStatus, TimelineTabs, + ScrollToTopEvent, } from '../../../../common/types/timeline'; import { PinnedEvent } from '../../../../common/types/timeline/pinned_event'; import type { TGridModelForTimeline } from '../../../../../timelines/public'; @@ -23,6 +24,9 @@ export type TimelineModel = TGridModelForTimeline & { /** The selected tab to displayed in the timeline */ activeTab: TimelineTabs; prevActiveTab: TimelineTabs; + + /** Used for scrolling to top when swiching tabs. It includes the timestamp of when the event happened */ + scrollToTop?: ScrollToTopEvent; /** Timeline saved object owner */ createdBy?: string; /** A summary of the events and notes in this timeline */ @@ -63,6 +67,8 @@ export type TimelineModel = TGridModelForTimeline & { status: TimelineStatus; /** updated saved object timestamp */ updated?: number; + /** updated saved object user */ + updatedBy?: string | null; /** timeline is saving */ isSaving: boolean; version: string | null; @@ -74,6 +80,7 @@ export type SubsetTimelineModel = Readonly< | 'activeTab' | 'prevActiveTab' | 'columns' + | 'defaultColumns' | 'dataProviders' | 'deletedEventIds' | 'description' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 8a5c8546d3834..96ae11cb8afdc 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -80,6 +80,7 @@ const basicTimeline: TimelineModel = { activeTab: TimelineTabs.query, prevActiveTab: TimelineTabs.graph, columns: [], + defaultColumns: [], dataProviders: [{ ...basicDataProvider }], dateRange: { start: '2020-07-07T08:20:18.966Z', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index a302f43e61b13..97fa72667a3c6 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -331,7 +331,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) }, }, })) - .case(setActiveTabTimeline, (state, { id, activeTab }) => ({ + .case(setActiveTabTimeline, (state, { id, activeTab, scrollToTop }) => ({ ...state, timelineById: { ...state.timelineById, @@ -339,6 +339,11 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state.timelineById[id], activeTab, prevActiveTab: state.timelineById[id].activeTab, + scrollToTop: scrollToTop + ? { + timestamp: Math.floor(Date.now() / 1000), // convert to seconds to avoid unnecessary rerenders for multiple clicks + } + : undefined, }, }, })) diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 326a6973db53b..968211a0c82df 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -9,6 +9,7 @@ import { CoreStart } from '../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { LensPublicStart } from '../../../plugins/lens/public'; import { NewsfeedPublicPluginStart } from '../../../../src/plugins/newsfeed/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; @@ -58,6 +59,7 @@ export interface StartPlugins { embeddable: EmbeddableStart; inspector: InspectorStart; fleet?: FleetStart; + lens: LensPublicStart; lists?: ListsPluginStart; licensing: LicensingPluginStart; newsfeed?: NewsfeedPublicPluginStart; diff --git a/x-pack/plugins/security_solution/scripts/beat_docs/build.js b/x-pack/plugins/security_solution/scripts/beat_docs/build.js index b8bcedda9356a..554581e26d30f 100644 --- a/x-pack/plugins/security_solution/scripts/beat_docs/build.js +++ b/x-pack/plugins/security_solution/scripts/beat_docs/build.js @@ -26,7 +26,7 @@ const zlib = require('zlib'); const OUTPUT_DIRECTORY = resolve('scripts', 'beat_docs'); const OUTPUT_SERVER_DIRECTORY = resolve('server', 'utils', 'beat_schema'); -const BEATS_VERSION = '7.12.0'; +const BEATS_VERSION = '7.14.0'; const beats = [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration.ts index 3dead29d54bbc..f9693c87631b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration.ts @@ -71,11 +71,15 @@ export const createMigration = async ({ enrichment.indicator.reference = indicator.event?.reference; enrichment.matched = indicator.matched; enrichment.indicator.remove("matched"); - ctx._source.threat.enrichments.add(enrichment); } ctx._source.threat.remove("indicator"); } + + // migrate status + if(ctx._source.signal?.status == "in-progress") { + ctx._source.signal.status = "acknowledged"; + } `, params: { version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index fb53550dba769..833a9084fdac6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -14,7 +14,7 @@ Object { "mappings": Object { "_meta": Object { "aliases_version": 1, - "version": 56, + "version": 57, }, "dynamic": false, "properties": Object { @@ -769,7 +769,6 @@ Object { "type": "text", }, }, - "ignore_above": 1024, "index": false, "type": "keyword", }, @@ -785,6 +784,10 @@ Object { "ignore_above": 1024, "type": "keyword", }, + "agent_id_status": Object { + "ignore_above": 1024, + "type": "keyword", + }, "category": Object { "ignore_above": 1024, "type": "keyword", @@ -827,7 +830,6 @@ Object { }, "original": Object { "doc_values": false, - "ignore_above": 1024, "index": false, "type": "keyword", }, @@ -932,6 +934,123 @@ Object { "ignore_above": 1, "type": "keyword", }, + "elf": Object { + "properties": Object { + "architecture": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "byte_order": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "cpu_type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "creation_date": Object { + "type": "date", + }, + "exports": Object { + "type": "flattened", + }, + "header": Object { + "properties": Object { + "abi_version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "class": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "data": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "entrypoint": Object { + "type": "long", + }, + "object_version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "os_abi": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + "imports": Object { + "type": "flattened", + }, + "sections": Object { + "properties": Object { + "chi2": Object { + "type": "long", + }, + "entropy": Object { + "type": "long", + }, + "flags": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "name": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "physical_offset": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "physical_size": Object { + "type": "long", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "virtual_address": Object { + "type": "long", + }, + "virtual_size": Object { + "type": "long", + }, + }, + "type": "nested", + }, + "segments": Object { + "properties": Object { + "sections": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + "type": "nested", + }, + "shared_libraries": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "telfhash": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, "extension": Object { "ignore_above": 1024, "type": "keyword", @@ -1719,10 +1838,6 @@ Object { "path": "signal.rule.from", "type": "alias", }, - "kibana.alert.rule.id": Object { - "path": "signal.rule.id", - "type": "alias", - }, "kibana.alert.rule.immutable": Object { "path": "signal.rule.immutable", "type": "alias", @@ -1915,6 +2030,10 @@ Object { "path": "signal.rule.updated_by", "type": "alias", }, + "kibana.alert.rule.uuid": Object { + "path": "signal.rule.id", + "type": "alias", + }, "kibana.alert.rule.version": Object { "path": "signal.rule.version", "type": "alias", @@ -1997,7 +2116,6 @@ Object { }, "original": Object { "doc_values": false, - "ignore_above": 1024, "index": false, "type": "keyword", }, @@ -2547,6 +2665,123 @@ Object { "ignore_above": 1024, "type": "keyword", }, + "elf": Object { + "properties": Object { + "architecture": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "byte_order": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "cpu_type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "creation_date": Object { + "type": "date", + }, + "exports": Object { + "type": "flattened", + }, + "header": Object { + "properties": Object { + "abi_version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "class": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "data": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "entrypoint": Object { + "type": "long", + }, + "object_version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "os_abi": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + "imports": Object { + "type": "flattened", + }, + "sections": Object { + "properties": Object { + "chi2": Object { + "type": "long", + }, + "entropy": Object { + "type": "long", + }, + "flags": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "name": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "physical_offset": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "physical_size": Object { + "type": "long", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "virtual_address": Object { + "type": "long", + }, + "virtual_size": Object { + "type": "long", + }, + }, + "type": "nested", + }, + "segments": Object { + "properties": Object { + "sections": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + "type": "nested", + }, + "shared_libraries": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "telfhash": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, "entity_id": Object { "ignore_above": 1024, "type": "keyword", @@ -2646,81 +2881,198 @@ Object { "ignore_above": 1024, "type": "keyword", }, - "entity_id": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "executable": Object { - "fields": Object { - "text": Object { - "norms": false, - "type": "text", - }, - }, - "ignore_above": 1024, - "type": "keyword", - }, - "exit_code": Object { - "type": "long", - }, - "hash": Object { - "properties": Object { - "md5": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "sha1": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "sha256": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "sha512": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "ssdeep": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - }, - "name": Object { - "fields": Object { - "text": Object { - "norms": false, - "type": "text", - }, - }, - "ignore_above": 1024, - "type": "keyword", - }, - "pe": Object { + "elf": Object { "properties": Object { "architecture": Object { "ignore_above": 1024, "type": "keyword", }, - "company": Object { + "byte_order": Object { "ignore_above": 1024, "type": "keyword", }, - "description": Object { + "cpu_type": Object { "ignore_above": 1024, "type": "keyword", }, - "file_version": Object { - "ignore_above": 1024, - "type": "keyword", + "creation_date": Object { + "type": "date", }, - "imphash": Object { - "ignore_above": 1024, - "type": "keyword", + "exports": Object { + "type": "flattened", }, - "original_file_name": Object { - "ignore_above": 1024, + "header": Object { + "properties": Object { + "abi_version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "class": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "data": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "entrypoint": Object { + "type": "long", + }, + "object_version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "os_abi": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + "imports": Object { + "type": "flattened", + }, + "sections": Object { + "properties": Object { + "chi2": Object { + "type": "long", + }, + "entropy": Object { + "type": "long", + }, + "flags": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "name": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "physical_offset": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "physical_size": Object { + "type": "long", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "virtual_address": Object { + "type": "long", + }, + "virtual_size": Object { + "type": "long", + }, + }, + "type": "nested", + }, + "segments": Object { + "properties": Object { + "sections": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + "type": "nested", + }, + "shared_libraries": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "telfhash": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + "entity_id": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "executable": Object { + "fields": Object { + "text": Object { + "norms": false, + "type": "text", + }, + }, + "ignore_above": 1024, + "type": "keyword", + }, + "exit_code": Object { + "type": "long", + }, + "hash": Object { + "properties": Object { + "md5": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "sha1": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "sha256": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "sha512": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "ssdeep": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + "name": Object { + "fields": Object { + "text": Object { + "norms": false, + "type": "text", + }, + }, + "ignore_above": 1024, + "type": "keyword", + }, + "pe": Object { + "properties": Object { + "architecture": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "company": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "description": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "file_version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "imphash": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "original_file_name": Object { + "ignore_above": 1024, "type": "keyword", }, "product": Object { @@ -3809,7 +4161,8 @@ Object { "type": "text", }, }, - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, }, }, @@ -3880,7 +4233,8 @@ Object { "type": "keyword", }, "directory": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "drive_letter": Object { "ignore_above": 1, @@ -4045,7 +4399,8 @@ Object { "type": "text", }, }, - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "size": Object { "type": "long", @@ -4057,7 +4412,8 @@ Object { "type": "text", }, }, - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "type": Object { "ignore_above": 1024, @@ -4098,7 +4454,8 @@ Object { "type": "geo_point", }, "name": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "postal_code": Object { "ignore_above": 1024, @@ -4165,94 +4522,23 @@ Object { "ignore_above": 1024, "type": "keyword", }, - "authentihash": Object { - "ignore_above": 1024, - "type": "keyword", - }, "company": Object { "ignore_above": 1024, "type": "keyword", }, - "compile_timestamp": Object { - "type": "date", - }, - "compiler": Object { - "properties": Object { - "name": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "version": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - }, - "creation_date": Object { - "type": "date", - }, - "debug": Object { - "properties": Object { - "offset": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "size": Object { - "type": "long", - }, - "timestamp": Object { - "type": "date", - }, - "type": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - "type": "nested", - }, "description": Object { "ignore_above": 1024, "type": "keyword", }, - "entry_point": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "exports": Object { - "ignore_above": 1024, - "type": "keyword", - }, "file_version": Object { "ignore_above": 1024, "type": "keyword", }, - "icon": Object { - "properties": Object { - "hash": Object { - "properties": Object { - "dhash": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - }, - }, - }, "imphash": Object { "ignore_above": 1024, "type": "keyword", }, - "imports": Object { - "type": "flattened", - }, - "machine_type": Object { - "ignore_above": 1024, - "type": "keyword", - }, "original_file_name": Object { - "type": "wildcard", - }, - "packers": Object { "ignore_above": 1024, "type": "keyword", }, @@ -4260,70 +4546,6 @@ Object { "ignore_above": 1024, "type": "keyword", }, - "resources": Object { - "properties": Object { - "chi2": Object { - "type": "long", - }, - "entropy": Object { - "type": "long", - }, - "filetype": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "language": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "sha256": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "type": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - "type": "nested", - }, - "rich_header": Object { - "properties": Object { - "hash": Object { - "properties": Object { - "md5": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - }, - }, - }, - "sections": Object { - "properties": Object { - "chi2": Object { - "type": "long", - }, - "entropy": Object { - "type": "float", - }, - "flags": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "name": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "raw_size": Object { - "type": "long", - }, - "virtual_address": Object { - "type": "long", - }, - }, - "type": "nested", - }, }, }, "port": Object { @@ -4346,7 +4568,8 @@ Object { "type": "keyword", }, "strings": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "type": Object { "ignore_above": 1024, @@ -4359,10 +4582,12 @@ Object { "type": "keyword", }, "key": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "path": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "value": Object { "ignore_above": 1024, @@ -4383,7 +4608,8 @@ Object { "url": Object { "properties": Object { "domain": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "extension": Object { "ignore_above": 1024, @@ -4400,7 +4626,8 @@ Object { "type": "text", }, }, - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "original": Object { "fields": Object { @@ -4409,14 +4636,16 @@ Object { "type": "text", }, }, - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "password": Object { "ignore_above": 1024, "type": "keyword", }, "path": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "port": Object { "type": "long", @@ -4426,7 +4655,8 @@ Object { "type": "keyword", }, "registered_domain": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "scheme": Object { "ignore_above": 1024, @@ -4463,7 +4693,8 @@ Object { "type": "keyword", }, "distinguished_name": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "locality": Object { "ignore_above": 1024, @@ -4524,7 +4755,8 @@ Object { "type": "keyword", }, "distinguished_name": Object { - "type": "wildcard", + "ignore_above": 1024, + "type": "keyword", }, "locality": Object { "ignore_above": 1024, @@ -4577,206 +4809,6 @@ Object { }, }, }, - "pe": Object { - "properties": Object { - "architecture": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "authentihash": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "company": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "compile_timestamp": Object { - "type": "date", - }, - "compiler": Object { - "properties": Object { - "name": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "version": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - }, - "creation_date": Object { - "type": "date", - }, - "debug": Object { - "properties": Object { - "offset": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "size": Object { - "type": "long", - }, - "timestamp": Object { - "type": "date", - }, - "type": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - "type": "nested", - }, - "description": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "entry_point": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "exports": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "file_version": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "icon": Object { - "properties": Object { - "hash": Object { - "properties": Object { - "dhash": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - }, - }, - }, - "imphash": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "imports": Object { - "type": "flattened", - }, - "machine_type": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "original_file_name": Object { - "type": "wildcard", - }, - "packers": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "product": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "resources": Object { - "properties": Object { - "chi2": Object { - "type": "long", - }, - "entropy": Object { - "type": "long", - }, - "filetype": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "language": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "sha256": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "type": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - "type": "nested", - }, - "rich_header": Object { - "properties": Object { - "hash": Object { - "properties": Object { - "md5": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - }, - }, - }, - "sections": Object { - "properties": Object { - "chi2": Object { - "type": "long", - }, - "entropy": Object { - "type": "float", - }, - "flags": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "name": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "raw_size": Object { - "type": "long", - }, - "virtual_address": Object { - "type": "long", - }, - }, - "type": "nested", - }, - }, - }, - "registry": Object { - "properties": Object { - "data": Object { - "properties": Object { - "bytes": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "strings": Object { - "type": "wildcard", - }, - "type": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - }, - "hive": Object { - "ignore_above": 1024, - "type": "keyword", - }, - "key": Object { - "type": "wildcard", - }, - "path": Object { - "type": "wildcard", - }, - "value": Object { - "ignore_above": 1024, - "type": "keyword", - }, - }, - }, }, "type": "nested", }, @@ -4784,6 +4816,50 @@ Object { "ignore_above": 1024, "type": "keyword", }, + "group": Object { + "properties": Object { + "alias": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "id": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "name": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "reference": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + "software": Object { + "properties": Object { + "id": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "name": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "platforms": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "reference": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, "tactic": Object { "properties": Object { "id": Object { @@ -5684,6 +5760,6 @@ Object { }, }, }, - "version": 56, + "version": 57, } `; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts index 7dad03ed7e14e..ab7ff26d9d875 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts @@ -89,7 +89,7 @@ export const createDetectionIndex = async ( ruleDataService: RuleDataPluginService, ruleRegistryEnabled: boolean ): Promise => { - const esClient = context.core.elasticsearch.client.asCurrentUser; + const esClient = context.core.elasticsearch.client.asInternalUser; const spaceId = siemClient.getSpaceId(); if (!siemClient) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json index 3d24384680f57..ea4dfb80c1564 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json @@ -2,7 +2,7 @@ "index_patterns": ["try-ecs-*"], "mappings": { "_meta": { - "version": "1.10.0" + "version": "1.11.0" }, "date_detection": false, "dynamic_templates": [ @@ -726,7 +726,6 @@ "type": "text" } }, - "ignore_above": 1024, "index": false, "type": "keyword" }, @@ -742,6 +741,10 @@ "ignore_above": 1024, "type": "keyword" }, + "agent_id_status": { + "ignore_above": 1024, + "type": "keyword" + }, "category": { "ignore_above": 1024, "type": "keyword" @@ -784,7 +787,6 @@ }, "original": { "doc_values": false, - "ignore_above": 1024, "index": false, "type": "keyword" }, @@ -889,6 +891,123 @@ "ignore_above": 1, "type": "keyword" }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "extension": { "ignore_above": 1024, "type": "keyword" @@ -1495,7 +1614,6 @@ }, "original": { "doc_values": false, - "ignore_above": 1024, "index": false, "type": "keyword" }, @@ -1981,6 +2099,123 @@ "ignore_above": 1024, "type": "keyword" }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "entity_id": { "ignore_above": 1024, "type": "keyword" @@ -2080,6 +2315,123 @@ "ignore_above": 1024, "type": "keyword" }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "entity_id": { "ignore_above": 1024, "type": "keyword" @@ -2796,9 +3148,722 @@ }, "threat": { "properties": { - "framework": { - "ignore_above": 1024, - "type": "keyword" + "enrichments": { + "properties": { + "indicator": { + "properties": { + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "confidence": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "signing_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "team_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "first_seen": { + "type": "date" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + }, + "ssdeep": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "last_seen": { + "type": "date" + }, + "marking": { + "properties": { + "tlp": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "modified_at": { + "type": "date" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "port": { + "type": "long" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "scanner_stats": { + "type": "long" + }, + "sightings": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + }, + "type": "object" + }, + "matched": { + "properties": { + "atomic": { + "ignore_above": 1024, + "type": "keyword" + }, + "field": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "index": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "software": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platforms": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } }, "tactic": { "properties": { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index d1a369d571d06..38a3612e5861d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -29,7 +29,7 @@ import aadFieldConversion from './signal_aad_mapping.json'; incremented by 10 in order to add "room" for the aforementioned patch release */ -export const SIGNALS_TEMPLATE_VERSION = 56; +export const SIGNALS_TEMPLATE_VERSION = 57; /** @constant @type {number} @@ -74,15 +74,6 @@ export const getSignalsTemplate = (index: string, spaceId: string, aadIndexAlias ...fieldAliases, ...getRbacRequiredFields(spaceId), signal: signalsMapping.mappings.properties.signal, - threat: { - ...ecsMapping.mappings.properties.threat, - properties: { - ...ecsMapping.mappings.properties.threat.properties, - enrichments: { - ...otherMapping.mappings.properties.threat.properties.enrichments, - }, - }, - }, }, _meta: { version: SIGNALS_TEMPLATE_VERSION, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/other_mappings.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/other_mappings.json index 3b1ae9a9caa54..b61ad2e43ac03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/other_mappings.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/other_mappings.json @@ -178,999 +178,6 @@ } } }, - "threat": { - "properties": { - "enrichments": { - "properties": { - "indicator": { - "properties": { - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "type": "wildcard" - } - } - } - } - }, - "confidence": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "file": { - "properties": { - "accessed": { - "type": "date" - }, - "attributes": { - "ignore_above": 1024, - "type": "keyword" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "signing_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "team_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "created": { - "type": "date" - }, - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "directory": { - "type": "wildcard" - }, - "drive_letter": { - "ignore_above": 1, - "type": "keyword" - }, - "elf": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "byte_order": { - "ignore_above": 1024, - "type": "keyword" - }, - "cpu_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "creation_date": { - "type": "date" - }, - "exports": { - "type": "flattened" - }, - "header": { - "properties": { - "abi_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "entrypoint": { - "type": "long" - }, - "object_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "os_abi": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "imports": { - "type": "flattened" - }, - "sections": { - "properties": { - "chi2": { - "type": "long" - }, - "entropy": { - "type": "long" - }, - "flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "physical_offset": { - "ignore_above": 1024, - "type": "keyword" - }, - "physical_size": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "virtual_address": { - "type": "long" - }, - "virtual_size": { - "type": "long" - } - }, - "type": "nested" - }, - "segments": { - "properties": { - "sections": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "shared_libraries": { - "ignore_above": 1024, - "type": "keyword" - }, - "telfhash": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mime_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "type": "wildcard" - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "type": "wildcard" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "first_seen": { - "type": "date" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "type": "wildcard" - }, - "postal_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - }, - "ssdeep": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "last_seen": { - "type": "date" - }, - "marking": { - "properties": { - "tlp": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "modified_at": { - "type": "date" - }, - "pe": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "authentihash": { - "ignore_above": 1024, - "type": "keyword" - }, - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "compile_timestamp": { - "type": "date" - }, - "compiler": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "creation_date": { - "type": "date" - }, - "debug": { - "properties": { - "offset": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "timestamp": { - "type": "date" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "entry_point": { - "ignore_above": 1024, - "type": "keyword" - }, - "exports": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "icon": { - "properties": { - "hash": { - "properties": { - "dhash": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "imports": { - "type": "flattened" - }, - "machine_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "type": "wildcard" - }, - "packers": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "resources": { - "properties": { - "chi2": { - "type": "long" - }, - "entropy": { - "type": "long" - }, - "filetype": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "rich_header": { - "properties": { - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "sections": { - "properties": { - "chi2": { - "type": "long" - }, - "entropy": { - "type": "float" - }, - "flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_size": { - "type": "long" - }, - "virtual_address": { - "type": "long" - } - }, - "type": "nested" - } - } - }, - "port": { - "type": "long" - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "registry": { - "properties": { - "data": { - "properties": { - "bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "strings": { - "type": "wildcard" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hive": { - "ignore_above": 1024, - "type": "keyword" - }, - "key": { - "type": "wildcard" - }, - "path": { - "type": "wildcard" - }, - "value": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "scanner_stats": { - "type": "long" - }, - "sightings": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "properties": { - "domain": { - "type": "wildcard" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "type": "wildcard" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "type": "wildcard" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "type": "wildcard" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "type": "wildcard" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "subdomain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "x509": { - "properties": { - "alternative_names": { - "ignore_above": 1024, - "type": "keyword" - }, - "issuer": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "type": "wildcard" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "public_key_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_exponent": { - "doc_values": false, - "index": false, - "type": "long" - }, - "public_key_size": { - "type": "long" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "type": "wildcard" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version_number": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - }, - "type": "object" - }, - "matched": { - "properties": { - "atomic": { - "ignore_above": 1024, - "type": "keyword" - }, - "field": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "index": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "authentihash": { - "ignore_above": 1024, - "type": "keyword" - }, - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "compile_timestamp": { - "type": "date" - }, - "compiler": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "creation_date": { - "type": "date" - }, - "debug": { - "properties": { - "offset": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "timestamp": { - "type": "date" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "entry_point": { - "ignore_above": 1024, - "type": "keyword" - }, - "exports": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "icon": { - "properties": { - "hash": { - "properties": { - "dhash": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "imports": { - "type": "flattened" - }, - "machine_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "type": "wildcard" - }, - "packers": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "resources": { - "properties": { - "chi2": { - "type": "long" - }, - "entropy": { - "type": "long" - }, - "filetype": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "rich_header": { - "properties": { - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "sections": { - "properties": { - "chi2": { - "type": "long" - }, - "entropy": { - "type": "float" - }, - "flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_size": { - "type": "long" - }, - "virtual_address": { - "type": "long" - } - }, - "type": "nested" - } - } - }, - "registry": { - "properties": { - "data": { - "properties": { - "bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "strings": { - "type": "wildcard" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hive": { - "ignore_above": 1024, - "type": "keyword" - }, - "key": { - "type": "wildcard" - }, - "path": { - "type": "wildcard" - }, - "value": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - }, - "type": "nested" - } - } - }, "vlan": { "properties": { "id": { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts index 4cfedd5dcaa01..c36dade4bb9d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts @@ -30,7 +30,7 @@ export const readIndexRoute = (router: SecuritySolutionPluginRouter, config: Con const siemResponse = buildSiemResponse(response); try { - const esClient = context.core.elasticsearch.client.asCurrentUser; + const esClient = context.core.elasticsearch.client.asInternalUser; const siemClient = context.securitySolution?.getAppClient(); if (!siemClient) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json index 68c184b66c562..8391d490162df 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json @@ -35,7 +35,7 @@ "signal.rule.enabled": "kibana.alert.rule.enabled", "signal.rule.false_positives": "kibana.alert.rule.false_positives", "signal.rule.from": "kibana.alert.rule.from", - "signal.rule.id": "kibana.alert.rule.id", + "signal.rule.id": "kibana.alert.rule.uuid", "signal.rule.immutable": "kibana.alert.rule.immutable", "signal.rule.index": "kibana.alert.rule.index", "signal.rule.interval": "kibana.alert.rule.interval", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts index 3f2f34c17679f..5445184c450fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts @@ -13,7 +13,7 @@ import { EVENT_KIND, SPACE_IDS, TIMESTAMP, - ALERT_RULE_ID, + ALERT_RULE_UUID, } from '@kbn/rule-data-utils'; import moment from 'moment'; @@ -98,7 +98,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { } const filter: estypes.QueryDslQueryContainer[] = [ - { terms: { [ALERT_RULE_ID]: ruleIds } }, + { terms: { [ALERT_RULE_UUID]: ruleIds } }, { terms: { [SPACE_IDS]: [spaceId] } }, ]; @@ -117,7 +117,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { aggs: { rules: { terms: { - field: ALERT_RULE_ID, + field: ALERT_RULE_UUID, size: ruleIds.length, }, aggs: { @@ -151,7 +151,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { bucket.most_recent_logs.hits.hits.map((event) => { const logEntry = parseRuleExecutionLog(event._source); invariant( - logEntry[ALERT_RULE_ID] ?? '', + logEntry[ALERT_RULE_UUID] ?? '', 'Malformed execution log entry: rule.id field not found' ); @@ -185,7 +185,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { ] : undefined; - const alertId = logEntry[ALERT_RULE_ID] ?? ''; + const alertId = logEntry[ALERT_RULE_UUID] ?? ''; const statusDate = logEntry[TIMESTAMP]; const lastFailureAt = lastFailure?.[TIMESTAMP]; const lastFailureMessage = lastFailure?.[MESSAGE]; @@ -232,7 +232,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { [EVENT_ACTION]: metric, [EVENT_KIND]: 'metric', [getMetricField(metric)]: value, - [ALERT_RULE_ID]: ruleId ?? '', + [ALERT_RULE_UUID]: ruleId ?? '', [TIMESTAMP]: new Date().toISOString(), [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [ALERT_RULE_TYPE_ID]: SERVER_APP_ID, @@ -255,7 +255,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { [EVENT_KIND]: 'event', [EVENT_SEQUENCE]: this.sequence++, [MESSAGE]: message, - [ALERT_RULE_ID]: ruleId ?? '', + [ALERT_RULE_UUID]: ruleId ?? '', [RULE_STATUS_SEVERITY]: statusSeverityDict[newStatus], [RULE_STATUS]: newStatus, [TIMESTAMP]: new Date().toISOString(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json index 1291c9274271e..ef3a3bef324f9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json @@ -32,7 +32,7 @@ { "feature": { "ml": ["all"], - "siem": ["all"], + "siem": ["all", "read_alerts", "crud_alerts"], "actions": ["read"], "builtInAlerts": ["all"], "dev_tools": ["all"] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json index 62d0343e57afb..f9d2c68e6878a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json @@ -37,7 +37,7 @@ { "feature": { "ml": ["read"], - "siem": ["all"], + "siem": ["all", "read_alerts", "crud_alerts"], "actions": ["read"], "builtInAlerts": ["all"] }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json index d5c84dc5811de..5c6188b053d20 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json @@ -32,7 +32,7 @@ { "feature": { "ml": ["all"], - "siem": ["all"], + "siem": ["all", "read_alerts", "crud_alerts"], "actions": ["all"], "builtInAlerts": ["all"] }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json index 72c9369b31ae8..d04251542d11b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json @@ -25,7 +25,7 @@ { "feature": { "ml": ["read"], - "siem": ["read"], + "siem": ["read", "read_alerts"], "actions": ["read"], "builtInAlerts": ["read"] }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json index 1f3b8ceaf7b36..f7b8818d6c004 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json @@ -35,7 +35,7 @@ { "feature": { "ml": ["read"], - "siem": ["all"], + "siem": ["all", "read_alerts", "crud_alerts"], "actions": ["read"], "builtInAlerts": ["all"] }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json index ae18fb7d3f1b5..324fb2737f24f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json @@ -35,7 +35,7 @@ { "feature": { "ml": ["read"], - "siem": ["all"], + "siem": ["all", "read_alerts", "crud_alerts"], "actions": ["all"], "builtInAlerts": ["all"] }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json index fad86e4a3572a..90232bdb53fed 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json @@ -25,7 +25,7 @@ { "feature": { "ml": ["read"], - "siem": ["read"], + "siem": ["read", "read_alerts"], "actions": ["read"], "builtInAlerts": ["read"] }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json index af3ba06a97d6a..9885ba0ee610b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json @@ -27,7 +27,7 @@ { "feature": { "ml": ["read"], - "siem": ["read"], + "siem": ["read", "read_alerts"], "actions": ["read"], "builtInAlerts": ["read"] }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts index b32bd64a07337..6af258a4cbe64 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts @@ -83,3 +83,12 @@ export function isPackagePolicyList( return (data as PackagePolicy[])[0].inputs !== undefined; } + +/** + * Convert counter label list to kebab case + * @params label_list the list of labels to create standardized UsageCounter from + * @returns a string label for usage in the UsageCounter + */ +export function createUsageCounterLabel(labelList: string[]): string { + return labelList.join('-'); +} diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index e6a53e520ee01..4e6520b67ab05 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -8,10 +8,15 @@ /* eslint-disable dot-notation */ import { TelemetryEventsSender, copyAllowlistedFields, getV3UrlFromV2 } from './sender'; import { loggingSystemMock } from 'src/core/server/mocks'; +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; import { URL } from 'url'; describe('TelemetryEventsSender', () => { let logger: ReturnType; + const usageCountersServiceSetup = usageCountersServiceMock.createSetupContract(); + const telemetryUsageCounter = usageCountersServiceSetup.createUsageCounter( + 'testTelemetryUsageCounter' + ); beforeEach(() => { logger = loggingSystemMock.createLogger(); @@ -163,19 +168,26 @@ describe('TelemetryEventsSender', () => { it('empties the queue when sending', async () => { const sender = new TelemetryEventsSender(logger); - sender['sendEvents'] = jest.fn(); sender['telemetryStart'] = { getIsOptedIn: jest.fn(async () => true), }; sender['telemetrySetup'] = { getTelemetryUrl: jest.fn(async () => new URL('https://telemetry.elastic.co')), }; + sender['telemetryUsageCounter'] = telemetryUsageCounter; sender['fetchClusterInfo'] = jest.fn(async () => { return { cluster_name: 'test', cluster_uuid: 'test-uuid', }; }); + sender['sendEvents'] = jest.fn(async () => { + sender['telemetryUsageCounter']?.incrementCounter({ + counterName: 'test_counter', + counterType: 'invoked', + incrementBy: 1, + }); + }); sender.queueTelemetryEvents([{ 'event.kind': '1' }, { 'event.kind': '2' }]); expect(sender['queue'].length).toBe(2); @@ -188,6 +200,7 @@ describe('TelemetryEventsSender', () => { await sender['sendIfDue'](); expect(sender['queue'].length).toBe(0); expect(sender['sendEvents']).toBeCalledTimes(2); + expect(sender['telemetryUsageCounter'].incrementCounter).toBeCalledTimes(2); }); it("shouldn't send when telemetry is disabled", async () => { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index 7dbef74da2fc8..5724c61bfcee7 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -12,11 +12,13 @@ import { SearchRequest } from '@elastic/elasticsearch/api/types'; import { URL } from 'url'; import { CoreStart, ElasticsearchClient, Logger } from 'src/core/server'; import { TelemetryPluginStart, TelemetryPluginSetup } from 'src/plugins/telemetry/server'; +import { UsageCounter } from 'src/plugins/usage_collection/server'; import { transformDataToNdjson } from '../../utils/read_stream/create_stream_from_ndjson'; import { TaskManagerSetupContract, TaskManagerStartContract, } from '../../../../task_manager/server'; +import { createUsageCounterLabel } from './helpers'; import { TelemetryDiagTask } from './diagnostic_task'; import { TelemetryEndpointTask } from './endpoint_task'; import { TelemetryTrustedAppsTask } from './trusted_apps_task'; @@ -27,6 +29,7 @@ import { getTrustedAppsList } from '../../endpoint/routes/trusted_apps/service'; type BaseSearchTypes = string | number | boolean | object; export type SearchTypes = BaseSearchTypes | BaseSearchTypes[] | undefined; +const usageLabelPrefix: string[] = ['security_telemetry', 'sender']; export interface TelemetryEvent { [key: string]: SearchTypes; @@ -66,13 +69,19 @@ export class TelemetryEventsSender { private esClient?: ElasticsearchClient; private savedObjectsClient?: SavedObjectsClientContract; private exceptionListClient?: ExceptionListClient; + private telemetryUsageCounter?: UsageCounter; constructor(logger: Logger) { this.logger = logger.get('telemetry_events'); } - public setup(telemetrySetup?: TelemetryPluginSetup, taskManager?: TaskManagerSetupContract) { + public setup( + telemetrySetup?: TelemetryPluginSetup, + taskManager?: TaskManagerSetupContract, + telemetryUsageCounter?: UsageCounter + ) { this.telemetrySetup = telemetrySetup; + this.telemetryUsageCounter = telemetryUsageCounter; if (taskManager) { this.diagTask = new TelemetryDiagTask(this.logger, taskManager, this); @@ -285,6 +294,16 @@ export class TelemetryEventsSender { } if (events.length > this.maxQueueSize - qlength) { + this.telemetryUsageCounter?.incrementCounter({ + counterName: createUsageCounterLabel(usageLabelPrefix.concat(['queue_stats'])), + counterType: 'docs_lost', + incrementBy: events.length, + }); + this.telemetryUsageCounter?.incrementCounter({ + counterName: createUsageCounterLabel(usageLabelPrefix.concat(['queue_stats'])), + counterType: 'num_capacity_exceeded', + incrementBy: 1, + }); this.queue.push(...this.processEvents(events.slice(0, this.maxQueueSize - qlength))); } else { this.queue.push(...this.processEvents(events)); @@ -344,6 +363,7 @@ export class TelemetryEventsSender { await this.sendEvents( toSend, telemetryUrl, + 'alerts-endpoint', clusterInfo.cluster_uuid, clusterInfo.version?.number, licenseInfo?.uid @@ -379,6 +399,7 @@ export class TelemetryEventsSender { await this.sendEvents( toSend, telemetryUrl, + channel, clusterInfo.cluster_uuid, clusterInfo.version?.number, licenseInfo?.uid @@ -429,6 +450,7 @@ export class TelemetryEventsSender { private async sendEvents( events: unknown[], telemetryUrl: string, + channel: string, clusterUuid: string, clusterVersionNumber: string | undefined, licenseId: string | undefined @@ -445,11 +467,31 @@ export class TelemetryEventsSender { ...(licenseId ? { 'X-Elastic-License-ID': licenseId } : {}), }, }); + this.telemetryUsageCounter?.incrementCounter({ + counterName: createUsageCounterLabel(usageLabelPrefix.concat(['payloads', channel])), + counterType: resp.status.toString(), + incrementBy: 1, + }); + this.telemetryUsageCounter?.incrementCounter({ + counterName: createUsageCounterLabel(usageLabelPrefix.concat(['payloads', channel])), + counterType: 'docs_sent', + incrementBy: events.length, + }); this.logger.debug(`Events sent!. Response: ${resp.status} ${JSON.stringify(resp.data)}`); } catch (err) { this.logger.warn( `Error sending events: ${err.response.status} ${JSON.stringify(err.response.data)}` ); + this.telemetryUsageCounter?.incrementCounter({ + counterName: createUsageCounterLabel(usageLabelPrefix.concat(['payloads', channel])), + counterType: 'docs_lost', + incrementBy: events.length, + }); + this.telemetryUsageCounter?.incrementCounter({ + counterName: createUsageCounterLabel(usageLabelPrefix.concat(['payloads', channel])), + counterType: 'num_exceptions', + incrementBy: 1, + }); } } } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 9176c7ee70ba1..e5c837293e2a8 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -21,7 +21,10 @@ import { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, } from '../../../../src/plugins/data/server'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { + UsageCollectionSetup, + UsageCounter, +} from '../../../../src/plugins/usage_collection/server'; import { PluginSetupContract as AlertingSetup, PluginStartContract as AlertPluginStartContract, @@ -143,6 +146,7 @@ export class Plugin implements IPlugin; + private telemetryUsageCounter?: UsageCounter; constructor(context: PluginInitializerContext) { this.context = context; @@ -181,6 +185,8 @@ export class Plugin implements IPlugin(); core.http.registerRouteHandlerContext( APP_ID, @@ -329,7 +335,11 @@ export class Plugin implements IPlugin ( instanceId: string @@ -53,7 +52,7 @@ describe('geo_containment', () => { it('should correctly transform expected results', async () => { const transformedResults = transformResults( // @ts-ignore - (sampleAggsJsonResponse.body as unknown) as SearchResponse, + sampleAggsJsonResponse.body, dateField, geoField ); @@ -113,7 +112,7 @@ describe('geo_containment', () => { it('should correctly transform expected results if fields are nested', async () => { const transformedResults = transformResults( // @ts-ignore - (sampleAggsJsonResponseWithNesting.body as unknown) as SearchResponse, + sampleAggsJsonResponseWithNesting.body, nestedDateField, nestedGeoField ); diff --git a/x-pack/plugins/task_manager/kibana.json b/x-pack/plugins/task_manager/kibana.json index aab1cd0ab41a5..d0b847ce58d77 100644 --- a/x-pack/plugins/task_manager/kibana.json +++ b/x-pack/plugins/task_manager/kibana.json @@ -2,6 +2,10 @@ "id": "taskManager", "server": true, "version": "8.0.0", + "owner": { + "name": "Kibana Alerting", + "githubTeam": "kibana-alerting-services" + }, "kibanaVersion": "kibana", "configPath": ["xpack", "task_manager"], "optionalPlugins": ["usageCollection"], diff --git a/x-pack/plugins/timelines/common/constants.ts b/x-pack/plugins/timelines/common/constants.ts index 9ef20f3ef5a6f..0c03682cc8332 100644 --- a/x-pack/plugins/timelines/common/constants.ts +++ b/x-pack/plugins/timelines/common/constants.ts @@ -12,6 +12,12 @@ export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern'; export const FILTER_OPEN: AlertStatus = 'open'; export const FILTER_CLOSED: AlertStatus = 'closed'; + +/** + * @deprecated + * TODO: Remove after `acknowledged` migration + */ export const FILTER_IN_PROGRESS: AlertStatus = 'in-progress'; +export const FILTER_ACKNOWLEDGED: AlertStatus = 'acknowledged'; export const RAC_ALERTS_BULK_UPDATE_URL = '/internal/rac/alerts/bulk_update'; diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index e61361233cda6..4d426272d8621 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -32,6 +32,8 @@ export interface ActionProps { isEventPinned?: boolean; isEventViewer?: boolean; rowIndex: number; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; refetch?: () => void; onRuleChange?: () => void; showNotes?: boolean; @@ -40,6 +42,25 @@ export interface ActionProps { toggleShowNotes?: () => void; } +export type SetEventsLoading = (params: { eventIds: string[]; isLoading: boolean }) => void; +export type SetEventsDeleted = (params: { eventIds: string[]; isDeleted: boolean }) => void; +export type OnUpdateAlertStatusSuccess = ( + updated: number, + conflicts: number, + status: AlertStatus +) => void; +export type OnUpdateAlertStatusError = (status: AlertStatus, error: Error) => void; + +export interface StatusBulkActionsProps { + eventIds: string[]; + currentStatus?: AlertStatus; + query?: string; + indexName: string; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; + onUpdateSuccess?: OnUpdateAlertStatusSuccess; + onUpdateFailure?: OnUpdateAlertStatusError; +} export interface HeaderActionProps { width: number; browserFields: BrowserFields; @@ -90,14 +111,17 @@ export type ControlColumnProps = Omit< keyof AdditionalControlColumnProps > & Partial; - -export type OnAlertStatusActionSuccess = (status: AlertStatus) => void; -export type OnAlertStatusActionFailure = (status: AlertStatus, error: string) => void; export interface BulkActionsObjectProp { alertStatusActions?: boolean; - onAlertStatusActionSuccess?: OnAlertStatusActionSuccess; - onAlertStatusActionFailure?: OnAlertStatusActionFailure; + onAlertStatusActionSuccess?: OnUpdateAlertStatusSuccess; + onAlertStatusActionFailure?: OnUpdateAlertStatusError; } export type BulkActionsProp = boolean | BulkActionsObjectProp; -export type AlertStatus = 'open' | 'closed' | 'in-progress'; +/** + * @deprecated + * TODO: remove when `acknowledged` migrations are finished + */ +export type InProgressStatus = 'in-progress'; + +export type AlertStatus = 'open' | 'closed' | 'acknowledged' | InProgressStatus; diff --git a/x-pack/plugins/timelines/kibana.json b/x-pack/plugins/timelines/kibana.json index bc9fba2c4a1bb..0239dcdd8f166 100644 --- a/x-pack/plugins/timelines/kibana.json +++ b/x-pack/plugins/timelines/kibana.json @@ -1,5 +1,9 @@ { "id": "timelines", + "owner": { + "name": "Security solution", + "githubTeam": "security-solution" + }, "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "timelines"], diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx index 338d7d1809074..19206c40d18c2 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.test.tsx @@ -11,6 +11,7 @@ import { TestProviders, mockGetAllCasesSelectorModal } from '../../../../mock'; import { AddToCaseAction } from './add_to_case_action'; import { SECURITY_SOLUTION_OWNER } from '../../../../../../cases/common'; import { AddToCaseActionButton } from './add_to_case_action_button'; +import { ALERT_RULE_UUID } from '@kbn/rule-data-utils'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ @@ -100,7 +101,7 @@ describe('AddToCaseAction', () => { {...props} event={{ _id: 'test-id', - data: [], + data: [{ field: ALERT_RULE_UUID, value: ['rule-id'] }], ecs: { _id: 'test-id', _index: 'test-index', @@ -112,7 +113,7 @@ describe('AddToCaseAction', () => { {...props} event={{ _id: 'test-id', - data: [], + data: [{ field: ALERT_RULE_UUID, value: ['rule-id'] }], ecs: { _id: 'test-id', _index: 'test-index', diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx index a292999ec75eb..b6d581f52cbe5 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx @@ -9,7 +9,7 @@ import React, { memo, useMemo, useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { CaseStatuses, StatusAll } from '../../../../../../cases/common'; import { TimelineItem } from '../../../../../common/'; -import { useAddToCase } from '../../../../hooks/use_add_to_case'; +import { useAddToCase, normalizedEventFields } from '../../../../hooks/use_add_to_case'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { TimelinesStartServices } from '../../../../types'; import { CreateCaseFlyout } from './create/flyout'; @@ -38,7 +38,6 @@ const AddToCaseActionComponent: React.FC = ({ }) => { const eventId = event?.ecs._id ?? ''; const eventIndex = event?.ecs._index ?? ''; - const rule = event?.ecs.signal?.rule; const dispatch = useDispatch(); const { cases } = useKibana().services; const { @@ -52,13 +51,14 @@ const AddToCaseActionComponent: React.FC = ({ } = useAddToCase({ event, useInsertTimeline, casePermissions, appId, onClose }); const getAllCasesSelectorModalProps = useMemo(() => { + const { ruleId, ruleName } = normalizedEventFields(event); return { alertData: { alertId: eventId, index: eventIndex ?? '', rule: { - id: rule?.id != null ? rule.id[0] : null, - name: rule?.name != null ? rule.name[0] : null, + id: ruleId, + name: ruleName, }, owner: appId, }, @@ -85,11 +85,10 @@ const AddToCaseActionComponent: React.FC = ({ goToCreateCase, eventId, eventIndex, - rule?.id, - rule?.name, appId, dispatch, useInsertTimeline, + event, ]); const closeCaseFlyoutOpen = useCallback(() => { diff --git a/x-pack/plugins/timelines/public/components/rule_name/index.tsx b/x-pack/plugins/timelines/public/components/rule_name/index.tsx new file mode 100644 index 0000000000000..2bfaf9b03525e --- /dev/null +++ b/x-pack/plugins/timelines/public/components/rule_name/index.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiLink } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import React, { useCallback, useMemo } from 'react'; +import { CoreStart } from '../../../../../../src/core/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +interface RuleNameProps { + name: string; + id: string; +} + +const appendSearch = (search?: string) => + isEmpty(search) ? '' : `${search?.startsWith('?') ? search : `?${search}`}`; + +const RuleNameComponents = ({ name, id }: RuleNameProps) => { + const { navigateToApp, getUrlForApp } = useKibana().services.application; + + const hrefRuleDetails = useMemo( + () => + getUrlForApp('securitySolution', { + deepLinkId: 'rules', + path: `/id/${id}${appendSearch(window.location.search)}`, + }), + [getUrlForApp, id] + ); + const goToRuleDetails = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp('securitySolution', { + deepLinkId: 'rules', + path: `/id/${id}${appendSearch(window.location.search)}`, + }); + }, + [navigateToApp, id] + ); + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + {name} + + ); +}; + +export const RuleName = React.memo(RuleNameComponents); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx index e263be11c0dcc..c5aba4506f39d 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx @@ -30,6 +30,8 @@ describe('checkbox control column', () => { rowIndex: 1, showNotes: true, timelineId: 'test-timelineId', + setEventsLoading: jest.fn(), + setEventsDeleted: jest.fn(), }; test('displays loader when id is included on loadingEventIds', () => { const { getByTestId } = render( diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx index e8459fa99d8c8..be7114be67b04 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx @@ -49,6 +49,8 @@ describe('Columns', () => { onRowSelected={jest.fn()} leadingControlColumns={[]} trailingControlColumns={[]} + setEventsLoading={jest.fn()} + setEventsDeleted={jest.fn()} /> ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx index 216e97d02a32a..597968679bd95 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx @@ -22,7 +22,11 @@ import { import { StatefulCell } from './stateful_cell'; import * as i18n from './translations'; -import { TimelineTabs } from '../../../../../common/types/timeline'; +import { + SetEventsDeleted, + SetEventsLoading, + TimelineTabs, +} from '../../../../../common/types/timeline'; import type { ActionProps, CellValueElementProps, @@ -69,6 +73,8 @@ interface DataDrivenColumnProps { timelineId: string; trailingControlColumns: ControlColumnProps[]; leadingControlColumns: ControlColumnProps[]; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; } const SPACE = ' '; @@ -140,6 +146,8 @@ const TgridActionTdCell = ({ tabType, timelineId, toggleShowNotes, + setEventsLoading, + setEventsDeleted, }: ActionProps & { columnId: string; hasRowRenderers: boolean; @@ -189,6 +197,8 @@ const TgridActionTdCell = ({ showNotes={showNotes} timelineId={timelineId} toggleShowNotes={toggleShowNotes} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> )} @@ -272,6 +282,8 @@ export const DataDrivenColumns = React.memo( timelineId, trailingControlColumns, leadingControlColumns, + setEventsLoading, + setEventsDeleted, }) => { const trailingActionCells = useMemo( () => @@ -312,6 +324,8 @@ export const DataDrivenColumns = React.memo( selectedEventIds={selectedEventIds} tabType={tabType} timelineId={timelineId} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> ) ); @@ -336,6 +350,8 @@ export const DataDrivenColumns = React.memo( tabType, timelineId, trailingActionCells, + setEventsLoading, + setEventsDeleted, ] ); const ColumnHeaders = useMemo( diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx index 23a66c9e18f7d..886c84dd32fb9 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx @@ -60,6 +60,8 @@ describe('EventColumnView', () => { isEventPinned: false, leadingControlColumns: [], trailingControlColumns: [], + setEventsLoading: jest.fn(), + setEventsDeleted: jest.fn(), }; // TODO: next 3 tests will be re-enabled in the future. diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx index aacb8dab32830..31123476a9a38 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx @@ -16,6 +16,8 @@ import type { ColumnHeaderOptions, ControlColumnProps, RowCellRender, + SetEventsDeleted, + SetEventsLoading, } from '../../../../../common/types/timeline'; import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; import type { Ecs } from '../../../../../common/ecs'; @@ -40,6 +42,8 @@ interface Props { timelineId: string; leadingControlColumns: ControlColumnProps[]; trailingControlColumns: ControlColumnProps[]; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; } export const EventColumnView = React.memo( @@ -63,6 +67,8 @@ export const EventColumnView = React.memo( timelineId, leadingControlColumns, trailingControlColumns, + setEventsLoading, + setEventsDeleted, }) => { // Each action button shall announce itself to screen readers via an `aria-label` // in the following format: @@ -120,6 +126,8 @@ export const EventColumnView = React.memo( onRuleChange={onRuleChange} tabType={tabType} timelineId={timelineId} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> )} @@ -143,6 +151,8 @@ export const EventColumnView = React.memo( showCheckboxes, tabType, timelineId, + setEventsLoading, + setEventsDeleted, ] ); return ( @@ -171,6 +181,8 @@ export const EventColumnView = React.memo( isEventViewer={isEventViewer} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx index cdd4a0a46656f..a5bc438ab251b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx @@ -18,7 +18,11 @@ import { StatefulRowRenderer } from './stateful_row_renderer'; import { getMappedNonEcsValue } from '../data_driven_columns'; import { StatefulEventContext } from './stateful_event_context'; import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; -import { TimelineTabs } from '../../../../../common/types/timeline'; +import { + SetEventsDeleted, + SetEventsLoading, + TimelineTabs, +} from '../../../../../common/types/timeline'; import type { CellValueElementProps, ColumnHeaderOptions, @@ -137,6 +141,20 @@ const StatefulEventComponent: React.FC = ({ ); }, [dispatch, event._id, event._index, tabType, timelineId]); + const setEventsLoading = useCallback( + ({ eventIds, isLoading }) => { + dispatch(tGridActions.setEventsLoading({ id: timelineId, eventIds, isLoading })); + }, + [dispatch, timelineId] + ); + + const setEventsDeleted = useCallback( + ({ eventIds, isDeleted }) => { + dispatch(tGridActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted })); + }, + [dispatch, timelineId] + ); + const RowRendererContent = useMemo( () => ( @@ -195,6 +213,8 @@ const StatefulEventComponent: React.FC = ({ timelineId={timelineId} leadingControlColumns={leadingControlColumns} trailingControlColumns={trailingControlColumns} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} />
{RowRendererContent}
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx index c9a94eab0ff20..2ab5a86fa7ddd 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx @@ -66,8 +66,10 @@ describe('Body', () => { excludedRowRendererIds: [], id: 'timeline-test', isSelectAllChecked: false, + itemsPerPageOptions: [], loadingEventIds: [], loadPage: jest.fn(), + querySize: 25, renderCellValue: TestCellRenderer, rowRenderers: [], selectedEventIds: {}, @@ -75,14 +77,15 @@ describe('Body', () => { sort: mockSort, showCheckboxes: false, tabType: TimelineTabs.query, + tableView: 'gridView', totalPages: 1, totalItems: 1, leadingControlColumns: [], trailingControlColumns: [], filterStatus: 'open', filterQuery: '', - indexNames: [''], refetch: jest.fn(), + indexNames: [''], }; describe('rendering', () => { diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 57f285542ed7a..8dbc7f34b530e 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -13,6 +13,8 @@ import { EuiDataGridStyle, EuiDataGridToolBarVisibilityOptions, EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import memoizeOne from 'memoize-one'; @@ -41,6 +43,8 @@ import { SortColumnTimeline, TimelineId, TimelineTabs, + SetEventsLoading, + SetEventsDeleted, } from '../../../../common/types/timeline'; import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; @@ -65,6 +69,8 @@ import * as i18n from './translations'; import { AlertCount } from '../styles'; import { checkBoxControlColumn } from './control_columns'; import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; +import { ViewSelection } from '../event_rendered_view/selector'; +import { EventRenderedView } from '../event_rendered_view'; const StatefulAlertStatusBulkActions = lazy( () => import('../toolbar/bulk_actions/alert_status_bulk_actions') @@ -74,25 +80,29 @@ interface OwnProps { activePage: number; additionalControls?: React.ReactNode; browserFields: BrowserFields; - filterQuery: string; + bulkActions?: BulkActionsProp; data: TimelineItem[]; defaultCellActions?: TGridCellAction[]; + filterQuery: string; + filterStatus?: AlertStatus; id: string; + indexNames: string[]; isEventViewer?: boolean; + itemsPerPageOptions: number[]; + leadingControlColumns?: ControlColumnProps[]; + loadPage: (newActivePage: number) => void; + onRuleChange?: () => void; + querySize: number; + refetch: Refetch; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; + tableView: ViewSelection; tabType: TimelineTabs; - leadingControlColumns?: ControlColumnProps[]; - loadPage: (newActivePage: number) => void; - trailingControlColumns?: ControlColumnProps[]; - totalPages: number; totalItems: number; - bulkActions?: BulkActionsProp; - filterStatus?: AlertStatus; + totalPages: number; + trailingControlColumns?: ControlColumnProps[]; unit?: (total: number) => React.ReactNode; - onRuleChange?: () => void; - indexNames: string[]; - refetch: Refetch; + hasAlertsCrud?: boolean; } const basicUnit = (n: number) => i18n.UNIT(n); @@ -131,6 +141,8 @@ const transformControlColumns = ({ browserFields, sort, theme, + setEventsLoading, + setEventsDeleted, }: { actionColumnsWidth: number; columnHeaders: ColumnHeaderOptions[]; @@ -149,6 +161,8 @@ const transformControlColumns = ({ onSelectPage: OnSelectAll; sort: SortColumnTimeline[]; theme: EuiTheme; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; }): EuiDataGridControlColumn[] => controlColumns.map( ({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({ @@ -208,6 +222,8 @@ const transformControlColumns = ({ tabType={tabType} timelineId={timelineId} width={width ?? MIN_ACTION_COLUMN_WIDTH} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> ); }, @@ -227,34 +243,38 @@ export const BodyComponent = React.memo( activePage, additionalControls, browserFields, - filterQuery, + bulkActions = true, + clearSelected, columnHeaders, data, defaultCellActions, excludedRowRendererIds, + filterQuery, + filterStatus, id, + indexNames, isEventViewer = false, isSelectAllChecked, + itemsPerPageOptions, + leadingControlColumns = EMPTY_CONTROL_COLUMNS, loadingEventIds, loadPage, - selectedEventIds, - setSelected, - clearSelected, onRuleChange, - showCheckboxes, + querySize, + refetch, renderCellValue, rowRenderers, + selectedEventIds, + setSelected, + showCheckboxes, sort, + tableView = 'gridView', tabType, - totalPages, totalItems, - filterStatus, - bulkActions = true, - unit = basicUnit, - leadingControlColumns = EMPTY_CONTROL_COLUMNS, + totalPages, trailingControlColumns = EMPTY_CONTROL_COLUMNS, - indexNames, - refetch, + unit = basicUnit, + hasAlertsCrud, }) => { const dispatch = useDispatch(); const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); @@ -319,6 +339,10 @@ export const BodyComponent = React.memo( }, [bulkActions]); const showBulkActions = useMemo(() => { + if (!hasAlertsCrud) { + return false; + } + if (selectedCount === 0 || !showCheckboxes) { return false; } @@ -326,7 +350,44 @@ export const BodyComponent = React.memo( return bulkActions; } return bulkActions.alertStatusActions ?? true; - }, [selectedCount, showCheckboxes, bulkActions]); + }, [hasAlertsCrud, selectedCount, showCheckboxes, bulkActions]); + + const alertToolbar = useMemo( + () => ( + + + {alertCountText} + + {showBulkActions && ( + }> + + + )} + + ), + [ + alertCountText, + filterQuery, + filterStatus, + id, + indexNames, + onAlertStatusActionFailure, + onAlertStatusActionSuccess, + refetch, + showBulkActions, + totalItems, + ] + ); const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo( () => ({ @@ -438,6 +499,20 @@ export const BodyComponent = React.memo( setVisibleColumns(columnHeaders.map(({ id: cid }) => cid)); }, [columnHeaders]); + const setEventsLoading = useCallback( + ({ eventIds, isLoading }) => { + dispatch(tGridActions.setEventsLoading({ id, eventIds, isLoading })); + }, + [dispatch, id] + ); + + const setEventsDeleted = useCallback( + ({ eventIds, isDeleted }) => { + dispatch(tGridActions.setEventsDeleted({ id, eventIds, isDeleted })); + }, + [dispatch, id] + ); + const [leadingTGridControlColumns, trailingTGridControlColumns] = useMemo(() => { return [ showCheckboxes ? [checkBoxControlColumn, ...leadingControlColumns] : leadingControlColumns, @@ -467,6 +542,8 @@ export const BodyComponent = React.memo( browserFields, onSelectPage, theme, + setEventsLoading, + setEventsDeleted, }) ); }, [ @@ -487,6 +564,8 @@ export const BodyComponent = React.memo( onSelectPage, sort, theme, + setEventsLoading, + setEventsDeleted, ]); const columnsWithCellActions: EuiDataGridColumn[] = useMemo( @@ -547,20 +626,39 @@ export const BodyComponent = React.memo( }, [columnHeaders, data, id, renderCellValue, tabType, theme, browserFields, rowRenderers]); return ( - + <> + {tableView === 'gridView' && ( + + )} + {tableView === 'eventRenderedView' && ( + + )} + ); } ); @@ -574,7 +672,10 @@ const makeMapStateToProps = () => { ) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders); const getTGrid = tGridSelectors.getTGridByIdSelector(); - const mapStateToProps = (state: TimelineState, { browserFields, id }: OwnProps) => { + const mapStateToProps = ( + state: TimelineState, + { browserFields, id, hasAlertsCrud }: OwnProps + ) => { const timeline: TGridModel = getTGrid(state, id); const { columns, @@ -593,7 +694,7 @@ const makeMapStateToProps = () => { loadingEventIds, id, selectedEventIds, - showCheckboxes, + showCheckboxes: hasAlertsCrud === true && showCheckboxes, sort, }; }; @@ -609,4 +710,4 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; -export const StatefulBody = connector(BodyComponent); +export const StatefulBody: React.FunctionComponent = connector(BodyComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx index 001578b01f09f..da3152509f5cf 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/row_action/index.tsx @@ -10,13 +10,15 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; -import { +import type { ColumnHeaderOptions, ControlColumnProps, OnRowSelected, + SetEventsLoading, + SetEventsDeleted, TimelineExpandedDetailType, - TimelineTabs, } from '../../../../../common/types/timeline'; +import { TimelineTabs } from '../../../../../common/types/timeline'; import { getMappedNonEcsValue } from '../data_driven_columns'; import { tGridActions } from '../../../../store/t_grid'; @@ -34,6 +36,8 @@ type Props = EuiDataGridCellValueElementProps & { tabType?: TimelineTabs; timelineId: string; width: number; + setEventsLoading: SetEventsLoading; + setEventsDeleted: SetEventsDeleted; }; const RowActionComponent = ({ @@ -50,6 +54,8 @@ const RowActionComponent = ({ showCheckboxes, tabType, timelineId, + setEventsLoading, + setEventsDeleted, width, }: Props) => { const { data: timelineNonEcsData, ecs: ecsData, _id: eventId, _index: indexName } = useMemo( @@ -120,6 +126,8 @@ const RowActionComponent = ({ tabType={tabType} timelineId={timelineId} width={width} + setEventsLoading={setEventsLoading} + setEventsDeleted={setEventsDeleted} /> )} diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx new file mode 100644 index 0000000000000..c52924d481aa0 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx @@ -0,0 +1,263 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + CriteriaWithPagination, + EuiBasicTable, + EuiBasicTableProps, + EuiDataGridCellValueElementProps, + EuiDataGridControlColumn, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + /* ALERT_REASON, ALERT_RULE_ID, */ ALERT_RULE_NAME, + TIMESTAMP, +} from '@kbn/rule-data-utils'; +import { get } from 'lodash'; +import moment from 'moment'; +import React, { ComponentType, useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; + +import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; + +import type { BrowserFields, RowRenderer, TimelineItem } from '../../../../common'; +import { tGridActions } from '../../../store/t_grid'; +import { RuleName } from '../../rule_name'; +import { isEventBuildingBlockType } from '../body/helpers'; + +const EventRenderedFlexItem = styled(EuiFlexItem)` + div:first-child { + padding-left: 0px; + div { + margin: 0px; + } + } +`; + +const ActionsContainer = styled.div` + display: flex; + align-items: center; + div div:first-child div.siemEventsTable__tdContent { + margin-left: ${({ theme }) => theme.eui.paddingSizes.m}; + } +`; + +// Fix typing issue with EuiBasicTable and styled +type BasicTableType = ComponentType>; + +const StyledEuiBasicTable = styled(EuiBasicTable as BasicTableType)` + padding-top: ${({ theme }) => theme.eui.paddingSizes.m}; + .EventRenderedView__buildingBlock { + background: ${({ theme }) => theme.eui.euiColorHighlight}; + } + + & > div:last-child { + height: 72px; + } +`; + +interface EventRenderedViewProps { + alertToolbar: React.ReactNode; + browserFields: BrowserFields; + events: TimelineItem[]; + leadingControlColumns: EuiDataGridControlColumn[]; + onChangePage: (newActivePage: number) => void; + pageIndex: number; + pageSize: number; + pageSizeOptions: number[]; + rowRenderers: RowRenderer[]; + timelineId: string; + totalItemCount: number; +} +const PreferenceFormattedDateComponent = ({ value }: { value: Date }) => { + const tz = useUiSetting('dateFormat:tz'); + const dateFormat = useUiSetting('dateFormat'); + return <>{moment.tz(value, tz).format(dateFormat)}; +}; +export const PreferenceFormattedDate = React.memo(PreferenceFormattedDateComponent); + +const EventRenderedViewComponent = ({ + alertToolbar, + browserFields, + events, + leadingControlColumns, + onChangePage, + pageIndex, + pageSize, + pageSizeOptions, + rowRenderers, + timelineId, + totalItemCount, +}: EventRenderedViewProps) => { + const dispatch = useDispatch(); + + const ActionTitle = useMemo( + () => ( + + {leadingControlColumns.map((action) => { + const ActionHeader = action.headerCellRender; + return ( + + + + ); + })} + + ), + [leadingControlColumns] + ); + + const columns = useMemo( + () => [ + { + field: 'actions', + name: ActionTitle, + truncateText: false, + hideForMobile: false, + // eslint-disable-next-line react/display-name + render: (name: unknown, item: unknown) => { + const alertId = get(item, '_id'); + const rowIndex = events.findIndex((evt) => evt._id === alertId); + return ( + + {leadingControlColumns.length > 0 + ? leadingControlColumns.map((action) => { + const getActions = action.rowCellRender as ( + props: EuiDataGridCellValueElementProps + ) => React.ReactNode; + return getActions({ + columnId: 'actions', + isDetails: false, + isExpandable: false, + isExpanded: false, + rowIndex, + setCellProps: () => null, + }); + }) + : null} + + ); + }, + width: '120px', + }, + { + field: 'ecs.@timestamp', + name: i18n.translate('xpack.timelines.alerts.EventRenderedView.timestamp.column', { + defaultMessage: 'Timestamp', + }), + truncateText: false, + hideForMobile: false, + // eslint-disable-next-line react/display-name + render: (name: unknown, item: TimelineItem) => { + const timestamp = get(item, `ecs.${TIMESTAMP}`); + return ; + }, + }, + { + field: `ecs.${ALERT_RULE_NAME}`, + name: i18n.translate('xpack.timelines.alerts.EventRenderedView.rule.column', { + defaultMessage: 'Rule', + }), + truncateText: false, + hideForMobile: false, + // eslint-disable-next-line react/display-name + render: (name: unknown, item: TimelineItem) => { + const ruleName = get(item, `ecs.signal.rule.name`); /* `ecs.${ALERT_RULE_NAME}`*/ + const ruleId = get(item, `ecs.signal.rule.id}`); /* `ecs.${ALERT_RULE_ID}`*/ + return ; + }, + }, + { + field: 'eventSummary', + name: i18n.translate('xpack.timelines.alerts.EventRenderedView.eventSummary.column', { + defaultMessage: 'Event Summary', + }), + truncateText: false, + hideForMobile: false, + // eslint-disable-next-line react/display-name + render: (name: unknown, item: TimelineItem) => { + const ecsData = get(item, 'ecs'); + const reason = get(item, `ecs.signal.reason`); /* `ecs.${ALERT_REASON}`*/ + const rowRenderersValid = rowRenderers.filter((rowRenderer) => + rowRenderer.isInstance(ecsData) + ); + return ( + + {reason && {reason}} + {rowRenderersValid.length > 0 && + rowRenderersValid.map((rowRenderer) => ( + <> + + + {rowRenderer.renderRow({ + browserFields, + data: ecsData, + isDraggable: false, + timelineId: 'NONE', + })} + + + ))} + + ); + }, + width: '60%', + }, + ], + [ActionTitle, browserFields, events, leadingControlColumns, rowRenderers] + ); + + const handleTableChange = useCallback( + (pageChange: CriteriaWithPagination) => { + if (pageChange.page.index !== pageIndex) { + onChangePage(pageChange.page.index); + } + if (pageChange.page.size !== pageSize) { + dispatch( + tGridActions.updateItemsPerPage({ id: timelineId, itemsPerPage: pageChange.page.size }) + ); + } + }, + [dispatch, onChangePage, pageIndex, pageSize, timelineId] + ); + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions, + hidePerPageOptions: false, + }), + [pageIndex, pageSize, pageSizeOptions, totalItemCount] + ); + + return ( + <> + {alertToolbar} + + isEventBuildingBlockType(ecs) + ? { + className: `EventRenderedView__buildingBlock`, + } + : {} + } + /> + + ); +}; + +export const EventRenderedView = React.memo(EventRenderedViewComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/selector/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/selector/index.tsx new file mode 100644 index 0000000000000..3ac2a87382b46 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/selector/index.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButtonEmpty, + EuiPopover, + EuiSelectable, + EuiSelectableOption, + EuiTitle, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +export type ViewSelection = 'gridView' | 'eventRenderedView'; + +const ContainerEuiSelectable = styled.div` + width: 300px; + .euiSelectableListItem__text { + white-space: pre-wrap !important; + line-height: normal; + } +`; + +const gridView = i18n.translate('xpack.timelines.alerts.summaryView.gridView.label', { + defaultMessage: 'Grid view', +}); + +const eventRenderedView = i18n.translate( + 'xpack.timelines.alerts.summaryView.eventRendererView.label', + { + defaultMessage: 'Event rendered view', + } +); + +interface SummaryViewSelectorProps { + onViewChange: (viewSelection: ViewSelection) => void; + viewSelected: ViewSelection; +} + +const SummaryViewSelectorComponent = ({ viewSelected, onViewChange }: SummaryViewSelectorProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = useCallback(() => setIsPopoverOpen((currentVal) => !currentVal), []); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const onChangeSelectable = useCallback( + (opts: EuiSelectableOption[]) => { + const selected = opts.filter((i) => i.checked === 'on'); + if (selected.length > 0) { + onViewChange((selected[0]?.key ?? 'gridView') as ViewSelection); + } + setIsPopoverOpen(false); + }, + [onViewChange] + ); + + const button = useMemo( + () => ( + + {viewSelected === 'gridView' ? gridView : eventRenderedView} + + ), + [onButtonClick, viewSelected] + ); + + const options = useMemo( + () => [ + { + label: gridView, + key: 'gridView', + checked: (viewSelected === 'gridView' ? 'on' : undefined) as EuiSelectableOption['checked'], + meta: [ + { + text: i18n.translate('xpack.timelines.alerts.summaryView.options.default.description', { + defaultMessage: + 'View as tabular data with the ability to group and sort by specific fields', + }), + }, + ], + }, + { + label: eventRenderedView, + key: 'eventRenderedView', + checked: (viewSelected === 'eventRenderedView' + ? 'on' + : undefined) as EuiSelectableOption['checked'], + meta: [ + { + text: i18n.translate( + 'xpack.timelines.alerts.summaryView.options.summaryView.description', + { + defaultMessage: 'View a rendering of the event flow for each alert', + } + ), + }, + ], + }, + ], + [viewSelected] + ); + + const renderOption = useCallback((option) => { + return ( + <> + +
{option.label}
+
+ + {option.meta[0].text} + + + ); + }, []); + + const listProps = useMemo( + () => ({ + rowHeight: 80, + showIcons: true, + }), + [] + ); + + return ( + + + + {(list) => list} + + + + ); +}; + +export const SummaryViewSelector = React.memo(SummaryViewSelectorComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap deleted file mode 100644 index d3d20c7183570..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HeaderSection it renders 1`] = ` -
- - - - - -

- Test title -

-
- -
-
-
-
-
-`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx deleted file mode 100644 index c5b4e679fe9f8..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, shallow } from 'enzyme'; -import React from 'react'; -import { TestProviders } from '../../../mock'; - -import { HeaderSection } from './index'; - -describe('HeaderSection', () => { - test('it renders', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the title', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-section-title"]').first().exists()).toBe(true); - }); - - test('it renders the subtitle when provided', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true); - }); - - test('renders the subtitle when not provided (to prevent layout thrash)', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true); - }); - - test('it renders supplements when children provided', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( - true - ); - }); - - test('it DOES NOT render supplements when children not provided', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( - false - ); - }); - - test('it applies border styles when border is true', () => { - const wrapper = mount( - - - - ); - const siemHeaderSection = wrapper.find('.siemHeaderSection').first(); - - expect(siemHeaderSection).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(siemHeaderSection).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); - }); - - test('it DOES NOT apply border styles when border is false', () => { - const wrapper = mount( - - - - ); - const siemHeaderSection = wrapper.find('.siemHeaderSection').first(); - - expect(siemHeaderSection).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(siemHeaderSection).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); - }); - - test('it splits the title and supplement areas evenly when split is true', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect( - wrapper - .find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]') - .first() - .exists() - ).toBe(false); - }); - - test('it DOES NOT split the title and supplement areas evenly when split is false', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect( - wrapper - .find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]') - .first() - .exists() - ).toBe(true); - }); - - test('it renders an inspect button when an `id` is provided', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(true); - }); - - test('it does NOT an inspect button when an `id` is NOT provided', () => { - const wrapper = mount( - - -

{'Test children'}

-
-
- ); - - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx deleted file mode 100644 index 3a6838f4d8640..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle, EuiTitleSize } from '@elastic/eui'; -import React from 'react'; -import styled, { css } from 'styled-components'; -import { InspectQuery } from '../../../store/t_grid/inputs'; -import { InspectButton } from '../../inspect'; - -import { Subtitle } from '../subtitle'; - -interface HeaderProps { - border?: boolean; - height?: number; -} - -const Header = styled.header.attrs(() => ({ - className: 'siemHeaderSection', -}))` - ${({ height }) => - height && - css` - height: ${height}px; - `} - margin-bottom: ${({ height, theme }) => (height ? 0 : theme.eui.euiSizeL)}; - user-select: text; - - ${({ border }) => - border && - css` - border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - padding-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; - `} -`; -Header.displayName = 'Header'; - -export interface HeaderSectionProps extends HeaderProps { - children?: React.ReactNode; - height?: number; - id?: string; - inspect: InspectQuery | null; - loading: boolean; - split?: boolean; - subtitle?: string | React.ReactNode; - title: string | React.ReactNode; - titleSize?: EuiTitleSize; - tooltip?: string; - growLeftSplit?: boolean; -} - -const HeaderSectionComponent: React.FC = ({ - border, - children, - height, - id, - inspect, - loading, - split, - subtitle, - title, - titleSize = 'm', - tooltip, - growLeftSplit = true, -}) => ( -
- - - - - -

- {title} - {tooltip && ( - <> - {' '} - - - )} -

-
- - -
- - {id && ( - - - - )} -
-
- - {children && ( - - {children} - - )} -
-
-); - -export const HeaderSection = React.memo(HeaderSectionComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index a7a80b5e61d2f..b84eff4d2142c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -47,19 +47,17 @@ import { } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; import { useTimelineEvents } from '../../../container'; -import { HeaderSection } from '../header_section'; import { StatefulBody } from '../body'; import { Footer, footerHeight } from '../footer'; -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles'; import * as i18n from '../translations'; -import { ExitFullScreen } from '../../exit_full_screen'; import { Sort } from '../body/sort'; -import { InspectButtonContainer } from '../../inspect'; +import { InspectButton, InspectButtonContainer } from '../../inspect'; +import { SummaryViewSelector, ViewSelection } from '../event_rendered_view/selector'; const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px -const COMPACT_HEADER_HEIGHT = 36; // px const TitleText = styled.span` margin-right: 12px; @@ -80,13 +78,10 @@ const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` `} `; -const TitleFlexGroup = styled(EuiFlexGroup)` - margin-top: 8px; -`; - const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, }))` + position: relative; width: 100%; overflow: hidden; flex: 1; @@ -104,14 +99,6 @@ const ScrollableFlexItem = styled(EuiFlexItem)` overflow: auto; `; -/** - * Hides stateful headerFilterGroup implementations, but prevents the component - * from being unmounted, to preserve the state of the component - */ -const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` - ${({ show }) => (show ? '' : 'visibility: hidden;')} -`; - const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM]; export interface TGridIntegratedProps { @@ -126,7 +113,6 @@ export interface TGridIntegratedProps { filters: Filter[]; globalFullScreen: boolean; graphOverlay?: React.ReactNode; - headerFilterGroup?: React.ReactNode; filterStatus?: AlertStatus; height?: number; id: TimelineId; @@ -150,6 +136,8 @@ export interface TGridIntegratedProps { leadingControlColumns?: ControlColumnProps[]; trailingControlColumns?: ControlColumnProps[]; data?: DataPublicPluginStart; + tGridEventRenderedViewEnabled: boolean; + hasAlertsCrud: boolean; } const TGridIntegratedComponent: React.FC = ({ @@ -163,7 +151,6 @@ const TGridIntegratedComponent: React.FC = ({ entityType, filters, globalFullScreen, - headerFilterGroup, filterStatus, id, indexNames, @@ -185,13 +172,16 @@ const TGridIntegratedComponent: React.FC = ({ graphEventId, leadingControlColumns, trailingControlColumns, + tGridEventRenderedViewEnabled, data, + hasAlertsCrud, }) => { const dispatch = useDispatch(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const { uiSettings } = useKibana().services; const [isQueryLoading, setIsQueryLoading] = useState(false); + const [tableView, setTableView] = useState('gridView'); const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); const unit = useMemo(() => (n: number) => i18n.ALERTS_UNIT(n), []); const { queryFields, title } = useDeepEqualSelector((state) => @@ -203,17 +193,6 @@ const TGridIntegratedComponent: React.FC = ({ }, [dispatch, id, isQueryLoading]); const justTitle = useMemo(() => {title}, [title]); - const titleWithExitFullScreen = useMemo( - () => ( - - {justTitle} - - - - - ), - [globalFullScreen, justTitle, setGlobalFullScreen] - ); const combinedQueries = buildCombinedQuery({ config: esQuery.getEsQueryConfig(uiSettings), @@ -255,6 +234,7 @@ const TGridIntegratedComponent: React.FC = ({ loading, { events, loadPage, pageInfo, refetch, totalCount = 0, inspect }, ] = useTimelineEvents({ + // We rely on entityType to determine Events vs Alerts alertConsumers: SECURITY_ALERTS_CONSUMERS, docValueFields, entityType, @@ -295,23 +275,12 @@ const TGridIntegratedComponent: React.FC = ({ events, ]); - const HeaderSectionContent = useMemo( - () => - headerFilterGroup && ( - - {headerFilterGroup} - - ), - [headerFilterGroup, graphEventId] - ); - useEffect(() => { setIsQueryLoading(loading); }, [loading]); + const alignItems = tableView === 'gridView' ? 'baseline' : 'center'; + return ( = ({ {canQueryTimeline ? ( <> - - {HeaderSectionContent} - - {graphOverlay} - + + + + {!resolverIsShowing(graphEventId) && additionalFilters} - + {tGridEventRenderedViewEnabled && entityType === 'alerts' && ( + + + + )} + = ({ ) : ( <> = ({ defaultCellActions={defaultCellActions} id={id} isEventViewer={true} + itemsPerPageOptions={itemsPerPageOptions} loadPage={loadPage} onRuleChange={onRuleChange} + querySize={pageInfo.querySize} renderCellValue={renderCellValue} rowRenderers={rowRenderers} tabType={TimelineTabs.query} + tableView={tableView} totalPages={calculateTotalPages({ itemsCount: totalCountMinusDeleted, itemsPerPage, @@ -399,19 +368,21 @@ const TGridIntegratedComponent: React.FC = ({ refetch={refetch} indexNames={indexNames} /> -