diff --git a/.browserslistrc b/.browserslistrc index 04395b913c9c5..36298c0f8cb93 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -4,6 +4,8 @@ last 2 Chrome versions last 2 Safari versions > 0.25% not ie 11 +not op_mini all +not samsung 4 [dev] last 1 chrome versions diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 525da9d832b53..36e9f4220b35c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -39,7 +39,10 @@ #CC# /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app #CC# /src/legacy/server/url_shortening/ @elastic/kibana-app #CC# /src/legacy/ui/public/state_management @elastic/kibana-app +#CC# /src/plugins/advanced_settings/ @elastic/kibana-app +#CC# /src/plugins/charts/ @elastic/kibana-app #CC# /src/plugins/index_pattern_management/public @elastic/kibana-app +#CC# /src/plugins/vis_default_editor @elastic/kibana-app # App Architecture /examples/bfetch_explorer/ @elastic/kibana-app-arch @@ -70,23 +73,10 @@ /x-pack/plugins/data_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-arch /x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch -#CC# /src/legacy/core_plugins/status_page/public @elastic/kibana-app-arch -#CC# /src/legacy/server/index_patterns/ @elastic/kibana-app-arch -#CC# /src/legacy/ui/public/field_editor @elastic/kibana-app-arch -#CC# /src/legacy/ui/public/management @elastic/kibana-app-arch -#CC# /src/plugins/advanced_settings/ @elastic/kibana-app-arch #CC# /src/plugins/bfetch/ @elastic/kibana-app-arch -#CC# /src/plugins/charts/ @elastic/kibana-app-arch #CC# /src/plugins/index_pattern_management/public/service @elastic/kibana-app-arch #CC# /src/plugins/inspector/ @elastic/kibana-app-arch -#CC# /src/plugins/saved_objects/ @elastic/kibana-app-arch #CC# /src/plugins/share/ @elastic/kibana-app-arch -#CC# /src/plugins/vis_default_editor @elastic/kibana-app-arch #CC# /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch #CC# /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch #CC# /packages/kbn-interpreter/ @elastic/kibana-app-arch @@ -171,8 +161,6 @@ /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis -#CC# /src/legacy/core_plugins/region_map @elastic/kibana-gis -#CC# /src/legacy/core_plugins/tile_map @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis #CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis @@ -245,6 +233,7 @@ #CC# /src/legacy/ui/public/documentation_links @elastic/kibana-platform #CC# /src/legacy/ui/public/autoload @elastic/kibana-platform #CC# /src/plugins/legacy_export/ @elastic/kibana-platform +#CC# /src/plugins/saved_objects/ @elastic/kibana-platform #CC# /src/plugins/status_page/ @elastic/kibana-platform #CC# /src/plugins/testbed/server/ @elastic/kibana-platform #CC# /x-pack/legacy/plugins/xpack_main/server/ @elastic/kibana-platform diff --git a/.github/ISSUE_TEMPLATE/Bug_report_security_solution.md b/.github/ISSUE_TEMPLATE/Bug_report_security_solution.md index 059e1d267c286..7a0514bca621d 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report_security_solution.md +++ b/.github/ISSUE_TEMPLATE/Bug_report_security_solution.md @@ -1,38 +1,38 @@ ---- -name: Bug report for Security Solution -about: Help us identify bugs in Elastic Security, SIEM, and Endpoint so we can fix them! -title: '[Security Solution]' -labels: 'Team: SecuritySolution' ---- - -**Describe the bug:** - -**Kibana/Elasticsearch Stack version:** - -**Server OS version:** - -**Browser and Browser OS versions:** - -**Elastic Endpoint version:** - -**Original install method (e.g. download page, yum, from source, etc.):** - -**Functional Area (e.g. Endpoint management, timelines, resolver, etc.):** - -**Steps to reproduce:** - -1. -2. -3. - -**Current behavior:** - -**Expected behavior:** - -**Screenshots (if relevant):** - -**Errors in browser console (if relevant):** - -**Provide logs and/or server output (if relevant):** - -**Any additional context (logs, chat logs, magical formulas, etc.):** +--- +name: Bug report for Security Solution +about: Help us identify bugs in Elastic Security, SIEM, and Endpoint so we can fix them! +title: '[Security Solution]' +labels: 'bug, Team: SecuritySolution' +--- + +**Describe the bug:** + +**Kibana/Elasticsearch Stack version:** + +**Server OS version:** + +**Browser and Browser OS versions:** + +**Elastic Endpoint version:** + +**Original install method (e.g. download page, yum, from source, etc.):** + +**Functional Area (e.g. Endpoint management, timelines, resolver, etc.):** + +**Steps to reproduce:** + +1. +2. +3. + +**Current behavior:** + +**Expected behavior:** + +**Screenshots (if relevant):** + +**Errors in browser console (if relevant):** + +**Provide logs and/or server output (if relevant):** + +**Any additional context (logs, chat logs, magical formulas, etc.):** diff --git a/.github/ISSUE_TEMPLATE/v8_breaking_change.md b/.github/ISSUE_TEMPLATE/v8_breaking_change.md index 99f779c288f5b..42783808e32ed 100644 --- a/.github/ISSUE_TEMPLATE/v8_breaking_change.md +++ b/.github/ISSUE_TEMPLATE/v8_breaking_change.md @@ -2,24 +2,35 @@ name: 8.0 Breaking change about: Breaking changes from 7.x -> 8.0 title: "[Breaking change]" -labels: Team:Elasticsearch UI, Feature:Upgrade Assistant +labels: Team:Elasticsearch UI, Feature:Upgrade Assistant, Breaking Change assignees: '' --- + + ## Change description **Which release will ship the breaking change?** - +8.0 **Describe the change. How will it manifest to users?** -**What percentage of users will be affected?** +**How many users will be affected?** - + + -**What can users to do to address the change manually?** +**What can users do to address the change manually?** diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 8e08c3806446d..4387e168f412d 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -38,8 +38,8 @@ NOTE: |The Charts plugin is a way to create easier integration of shared colors, themes, types and other utilities across all Kibana charts and visualizations. -|{kib-repo}blob/{branch}/src/plugins/console[console] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/console/README.md[console] +|Console provides the user with tools for storing and executing requests against Elasticsearch. |<> @@ -134,8 +134,8 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |WARNING: Missing README. -|{kib-repo}blob/{branch}/src/plugins/maps_legacy[mapsLegacy] -|WARNING: Missing README. +|{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] @@ -147,8 +147,8 @@ It also provides a stateful version of it on the start contract. |WARNING: Missing README. -|{kib-repo}blob/{branch}/src/plugins/region_map[regionMap] -|WARNING: Missing README. +|{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[savedObjects] @@ -180,8 +180,8 @@ so they can properly protect the data within their clusters. |This plugin adds the Advanced Settings section for the Usage and Security Data collection (aka Telemetry). -|{kib-repo}blob/{branch}/src/plugins/tile_map[tileMap] -|WARNING: Missing README. +|{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] @@ -307,8 +307,8 @@ Failure to have auth enabled in Kibana will make for a broken UI. UI-based error |WARNING: Missing README. -|{kib-repo}blob/{branch}/x-pack/plugins/console_extensions[consoleExtensions] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/console_extensions/README.md[consoleExtensions] +|This plugin provides autocomplete definitions of licensed APIs to the OSS Console plugin. |{kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] @@ -323,8 +323,8 @@ Failure to have auth enabled in Kibana will make for a broken UI. UI-based error |The deprecated dashboard only mode. -|{kib-repo}blob/{branch}/x-pack/plugins/data_enhanced[dataEnhanced] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/data_enhanced/README.md[dataEnhanced] +|The data_enhanced plugin is the x-pack counterpart to the OSS data plugin. |{kib-repo}blob/{branch}/x-pack/plugins/discover_enhanced/README.md[discoverEnhanced] @@ -355,8 +355,8 @@ and actions. |WARNING: Missing README. -|{kib-repo}blob/{branch}/x-pack/plugins/file_upload[fileUpload] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/file_upload/README.md[fileUpload] +|Backend and core front-end react-components for GeoJson file upload. Only supports the Maps plugin. |{kib-repo}blob/{branch}/x-pack/plugins/global_search/README.md[globalSearch] @@ -376,8 +376,9 @@ or dashboards from the Kibana instance, from both server and client-side plugins |This is the main source folder of the Graph plugin. It contains all of the Kibana server and client source code. x-pack/test/functional/apps/graph contains additional functional tests. -|{kib-repo}blob/{branch}/x-pack/plugins/grokdebugger[grokdebugger] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/grokdebugger/README.md[grokdebugger] +|This plugin helps users define Grok patterns, +which are particularly useful for ingesting logs. |{kib-repo}blob/{branch}/x-pack/plugins/index_lifecycle_management/README.md[indexLifecycleManagement] @@ -406,8 +407,8 @@ the infrastructure monitoring use-case within Kibana. |Run all tests from the x-pack root directory -|{kib-repo}blob/{branch}/x-pack/plugins/license_management[licenseManagement] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/license_management/README.md[licenseManagement] +|This plugin enables users to activate a trial license, downgrade to Basic, and upload a new license. |{kib-repo}blob/{branch}/x-pack/plugins/licensing/README.md[licensing] @@ -444,12 +445,12 @@ Elastic. |This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. -|{kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/painless_lab/README.md[painlessLab] +|This plugin helps users learn how to use the Painless scripting language. -|{kib-repo}blob/{branch}/x-pack/plugins/remote_clusters[remoteClusters] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/remote_clusters/README.md[remoteClusters] +|This plugin helps users manage their remote clusters, which enable cross-cluster search and cross-cluster replication. |{kib-repo}blob/{branch}/x-pack/plugins/reporting/README.md[reporting] @@ -460,8 +461,11 @@ Elastic. |Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. -|{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler[searchprofiler] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler/README.md[searchprofiler] +|The search profiler consumes the Profile API +by sending a search API with profile: true enabled in the request body. The response contains +detailed information on how Elasticsearch executed the search request. People use this information +to understand why a search request might be slow. |{kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] @@ -488,8 +492,8 @@ the alertTypes by the Stack in the alerting plugin, register associated HTTP routes, etc. -|{kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/task_manager/README.md[taskManager] +|The task manager is a generic system for running background tasks. |{kib-repo}blob/{branch}/x-pack/plugins/telemetry_collection_xpack/README.md[telemetryCollectionXpack] @@ -513,8 +517,9 @@ As a developer you can reuse and extend built-in alerts and actions UI functiona |Registers commercially licensed generic actions like per panel time range and contains some code that supports drilldown work. -|{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant[upgradeAssistant] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/upgrade_assistant/README.md[upgradeAssistant] +|Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. Its primary +purposes are to: |{kib-repo}blob/{branch}/x-pack/plugins/uptime/README.md[uptime] diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md index 1134994faa9bd..4129662acb2b1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md @@ -30,9 +30,9 @@ export declare class KibanaRequestboolean | Whether or not the request is a "system request" rather than an application-level request. Can be set on the client using the HttpFetchOptions#asSystemRequest option. | | [params](./kibana-plugin-core-server.kibanarequest.params.md) | | Params | | | [query](./kibana-plugin-core-server.kibanarequest.query.md) | | Query | | -| [rewrittenUrl](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | Url | URL rewritten in onPreRouting request interceptor. | +| [rewrittenUrl](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | URL | URL rewritten in onPreRouting request interceptor. | | [route](./kibana-plugin-core-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute<Method>> | matched route details | | [socket](./kibana-plugin-core-server.kibanarequest.socket.md) | | IKibanaSocket | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | -| [url](./kibana-plugin-core-server.kibanarequest.url.md) | | Url | a WHATWG URL standard object. | +| [url](./kibana-plugin-core-server.kibanarequest.url.md) | | URL | a WHATWG URL standard object. | | [uuid](./kibana-plugin-core-server.kibanarequest.uuid.md) | | string | A UUID to identify this request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md index 10628bafaf1d4..fb547330ee6ea 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.rewrittenurl.md @@ -9,5 +9,5 @@ URL rewritten in onPreRouting request interceptor. Signature: ```typescript -readonly rewrittenUrl?: Url; +readonly rewrittenUrl?: URL; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.url.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.url.md index 31d1348197201..b72760e272bb2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.url.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.url.md @@ -9,5 +9,5 @@ a WHATWG URL standard object. Signature: ```typescript -readonly url: Url; +readonly url: URL; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index bbf856480aedd..b2f8e83d8e654 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -17,6 +17,6 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | -| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management | +| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md index 7f39d9714a3a3..739fdfdeb5fc3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md @@ -4,7 +4,7 @@ ## ISearchSetup.session property -session management +session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index 4a69e94dd6f58..dba60c7bdf147 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,6 +19,6 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | -| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management | +| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md index de25cccd6d27a..1ad194a9bec86 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md @@ -4,7 +4,7 @@ ## ISearchStart.session property -session management +session management [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md new file mode 100644 index 0000000000000..fc3d214eb4cad --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.clear.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) + +## ISessionService.clear property + +Clears the active session. + +Signature: + +```typescript +clear: () => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md new file mode 100644 index 0000000000000..e30c89fb1a9fd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsession_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) + +## ISessionService.getSession$ property + +Returns the observable that emits an update every time the session ID changes + +Signature: + +```typescript +getSession$: () => Observable; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md new file mode 100644 index 0000000000000..838023ff1d8b9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.getsessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) + +## ISessionService.getSessionId property + +Returns the active session ID + +Signature: + +```typescript +getSessionId: () => string | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md new file mode 100644 index 0000000000000..174f9dbe66bf4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) + +## ISessionService interface + +Signature: + +```typescript +export interface ISessionService +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) | () => void | Clears the active session. | +| [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) | () => Observable<string | undefined> | Returns the observable that emits an update every time the session ID changes | +| [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) | () => string | undefined | Returns the active session ID | +| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => void | Restores existing session | +| [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) | () => string | Starts a new session | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md new file mode 100644 index 0000000000000..857e85bbd30eb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) + +## ISessionService.restore property + +Restores existing session + +Signature: + +```typescript +restore: (sessionId: string) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md new file mode 100644 index 0000000000000..9e14c5ed26765 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.start.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) + +## ISessionService.start property + +Starts a new session + +Signature: + +```typescript +start: () => string; +``` 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 6a3c437305cc8..ac6923fd12f96 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 @@ -74,6 +74,7 @@ | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | | [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | search service | | [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service | +| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) | | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md index d1d97d50f5948..f36f7b4ee77a4 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md @@ -19,5 +19,6 @@ export declare type EmbeddableInput = { timeRange?: TimeRange; query?: Query; filters?: Filter[]; + searchSessionId?: string; }; ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md new file mode 100644 index 0000000000000..62610624655a1 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) + +## isContextMenuTriggerContext variable + +Signature: + +```typescript +isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md index df67eda5074b9..06f792837e4fe 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md @@ -77,6 +77,7 @@ | [contextMenuTrigger](./kibana-plugin-plugins-embeddable-public.contextmenutrigger.md) | | | [defaultEmbeddableFactoryProvider](./kibana-plugin-plugins-embeddable-public.defaultembeddablefactoryprovider.md) | | | [EmbeddableRenderer](./kibana-plugin-plugins-embeddable-public.embeddablerenderer.md) | Helper react component to render an embeddable Can be used if you have an embeddable object or an embeddable factory Supports updating input by passing input prop | +| [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) | | | [isRangeSelectTriggerContext](./kibana-plugin-plugins-embeddable-public.israngeselecttriggercontext.md) | | | [isValueClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isvalueclicktriggercontext.md) | | | [PANEL\_BADGE\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_badge_trigger.md) | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md index aefd04112dc1c..3cc38a0cbdc0f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md @@ -39,6 +39,8 @@ export declare class Executor = Record + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [migrate](./kibana-plugin-plugins-expressions-public.executor.migrate.md) + +## Executor.migrate() method + +Signature: + +```typescript +migrate(ast: SerializableState, version: string): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | SerializableState | | +| version | string | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.migratetolatest.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.migratetolatest.md new file mode 100644 index 0000000000000..23b7e6035a0ae --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.migratetolatest.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Executor](./kibana-plugin-plugins-expressions-public.executor.md) > [migrateToLatest](./kibana-plugin-plugins-expressions-public.executor.migratetolatest.md) + +## Executor.migrateToLatest() method + +Signature: + +```typescript +migrateToLatest(ast: unknown, version: string): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | unknown | | +| version | string | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md index 1815d63d804b1..3e75e9ab3ef6f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md @@ -29,6 +29,7 @@ export declare class ExpressionFunction implements PersistableStatestring | A short help text. | | [inject](./kibana-plugin-plugins-expressions-public.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | +| [migrations](./kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableState) => SerializableState;
} | | | [name](./kibana-plugin-plugins-expressions-public.expressionfunction.name.md) | | string | Name of function | | [telemetry](./kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-public.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md new file mode 100644 index 0000000000000..28d521f4b3fe1 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-public.expressionfunction.md) > [migrations](./kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md) + +## ExpressionFunction.migrations property + +Signature: + +```typescript +migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md index 041d66b22dd50..307fc73ec6e9c 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md @@ -39,6 +39,8 @@ export declare class ExpressionsService implements PersistableStateExpressionsServiceStart['getType'] | | | [getTypes](./kibana-plugin-plugins-expressions-public.expressionsservice.gettypes.md) | | () => ReturnType<Executor['getTypes']> | Returns POJO map of all registered expression types, where keys are names of the types and values are ExpressionType instances. | | [inject](./kibana-plugin-plugins-expressions-public.expressionsservice.inject.md) | | (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression | Injects saved object references into expression AST | +| [migrate](./kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md) | | (state: SerializableState, version: string) => ExpressionAstExpression | Injects saved object references into expression AST | +| [migrateToLatest](./kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md) | | (state: unknown, version: string) => ExpressionAstExpression | Injects saved object references into expression AST | | [registerFunction](./kibana-plugin-plugins-expressions-public.expressionsservice.registerfunction.md) | | (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void | Register an expression function, which will be possible to execute as part of the expression pipeline.Below we register a function which simply sleeps for given number of milliseconds to delay the execution and outputs its input as-is. ```ts expressions.registerFunction({ diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md new file mode 100644 index 0000000000000..88a6bda4ee3f5 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [migrate](./kibana-plugin-plugins-expressions-public.expressionsservice.migrate.md) + +## ExpressionsService.migrate property + +Injects saved object references into expression AST + +Signature: + +```typescript +readonly migrate: (state: SerializableState, version: string) => ExpressionAstExpression; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md new file mode 100644 index 0000000000000..e6860df19fd3f --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) > [migrateToLatest](./kibana-plugin-plugins-expressions-public.expressionsservice.migratetolatest.md) + +## ExpressionsService.migrateToLatest property + +Injects saved object references into expression AST + +Signature: + +```typescript +readonly migrateToLatest: (state: unknown, version: string) => ExpressionAstExpression; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrenderer.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrenderer.md index 66c2e1e3c0c8d..32a7151578658 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrenderer.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrenderer.md @@ -7,5 +7,5 @@ Signature: ```typescript -ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element +ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md new file mode 100644 index 0000000000000..3f7eb12fbb7a8 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ReactExpressionRendererProps](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) > [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md) + +## ReactExpressionRendererProps.debounce property + +Signature: + +```typescript +debounce?: number; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md index 5622516530edd..e4980ce04b9e2 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md @@ -16,6 +16,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams | --- | --- | --- | | [className](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.classname.md) | string | | | [dataAttrs](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.dataattrs.md) | string[] | | +| [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md) | number | | | [expression](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.expression.md) | string | ExpressionAstExpression | | | [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | (event: ExpressionRendererEvent) => void | | | [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | 'xs' | 's' | 'm' | 'l' | 'xl' | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md index 97bb3ac895084..da20ae4aa892e 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md @@ -39,6 +39,8 @@ export declare class Executor = Record + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [migrate](./kibana-plugin-plugins-expressions-server.executor.migrate.md) + +## Executor.migrate() method + +Signature: + +```typescript +migrate(ast: SerializableState, version: string): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | SerializableState | | +| version | string | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.migratetolatest.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.migratetolatest.md new file mode 100644 index 0000000000000..72e3f8d8b7edc --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.migratetolatest.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Executor](./kibana-plugin-plugins-expressions-server.executor.md) > [migrateToLatest](./kibana-plugin-plugins-expressions-server.executor.migratetolatest.md) + +## Executor.migrateToLatest() method + +Signature: + +```typescript +migrateToLatest(ast: unknown, version: string): ExpressionAstExpression; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | unknown | | +| version | string | | + +Returns: + +`ExpressionAstExpression` + diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md index 7fcda94968d13..00c8aa63bfbd8 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md @@ -29,6 +29,7 @@ export declare class ExpressionFunction implements PersistableStatestring | A short help text. | | [inject](./kibana-plugin-plugins-expressions-server.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | +| [migrations](./kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableState) => SerializableState;
} | | | [name](./kibana-plugin-plugins-expressions-server.expressionfunction.name.md) | | string | Name of function | | [telemetry](./kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-server.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md new file mode 100644 index 0000000000000..29031a9306b2f --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExpressionFunction](./kibana-plugin-plugins-expressions-server.expressionfunction.md) > [migrations](./kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md) + +## ExpressionFunction.migrations property + +Signature: + +```typescript +migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; +``` diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 293597685ecc0..dbac6997ff433 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -11,6 +11,13 @@ values. . Enter a new value for the setting. . Click *Save changes*. +[float] +=== Required permissions + +The `Advanced Settings` {kib} privilege is required to access *Advanced Settings*. + +To add the privilege, open the menu, then click *Stack Management > Roles*. + [float] [[settings-read-only-access]] diff --git a/docs/management/alerting/alerts-and-actions-intro.asciidoc b/docs/management/alerting/alerts-and-actions-intro.asciidoc index 0c7ca7f1db17d..35a2452e99951 100644 --- a/docs/management/alerting/alerts-and-actions-intro.asciidoc +++ b/docs/management/alerting/alerts-and-actions-intro.asciidoc @@ -24,3 +24,8 @@ The *Alerts and Actions* UI only shows alerts and connectors for the current spa can be managed through the <>. See <> for more information. ============================================================================ + +[float] +=== Required permissions + +Access to alerts and actions is granted based on your privileges to alerting-enabled features. See <> for more information. diff --git a/docs/management/managing-beats.asciidoc b/docs/management/managing-beats.asciidoc index 10c98cca26345..232efb60cadd3 100644 --- a/docs/management/managing-beats.asciidoc +++ b/docs/management/managing-beats.asciidoc @@ -29,6 +29,13 @@ more information, see https://www.elastic.co/subscriptions and enrollment and configuration process step by step the first time you use the Central Management UI. +[float] +=== Required permissions + +You must have the `beats_admin` role assigned to use **{beats} Central Management** + +To assign the role, open the menu, then click *Stack Management > Users*. + [float] === Enroll {beats} diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 441bce43c7cdf..3734655edd91b 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -7,6 +7,13 @@ the index patterns that retrieve your data from {es}. [role="screenshot"] image::images/management-index-patterns.png[] +[float] +=== Required permissions + +The `Index Pattern Management` {kib} privilege is required to access the *Index patterns* UI. + +To add the privilege, open the menu, then click *Stack Management > Roles*. + [float] === Create an index pattern diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 639be87c540fb..2e081c09e0e70 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -10,6 +10,16 @@ To get started, open the main menu, then click *Stack Management > Saved Objects [role="screenshot"] image::images/management-saved-objects.png[Saved Objects] +[float] +=== Required permissions + +The `Saved Objects Management` {kib} privilege is required to access the *Saved Objects* UI. + +To add the privilege, open the menu, then click *Stack Management > Roles*. + +NOTE: +Granting access to Saved Objects Management will authorize users to manage all saved objects in {kib}, including objects that are managed by applications they may not otherwise be authorized to access. + [float] [[managing-saved-objects-view]] diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 0cb28ce0fb6e7..ef76121b21d29 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -167,4 +167,14 @@ The `xpack.` prefix has been removed for all telemetry configurations. *Impact:* For any configurations beginning with `xpack.telemetry`, remove the `xpack` prefix. Use {kibana-ref}/telemetry-settings-kbn.html#telemetry-general-settings[`telemetry.enabled`] instead. +[float] +=== SysV init support has been removed + +*Details:* +All supported operating systems support using systemd service files. Any system that doesn't already have service aliased to use kibana.service should use `systemctl start kibana.service` instead of the `service start kibana`. + +*Impact:* +Any installations using `.deb` or `.rpm` packages using SysV will need to migrate to systemd. + + // end::notable-breaking-changes[] diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 9054a97c90496..aa680720fc8ff 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -43,6 +43,12 @@ Changing these settings may disable features of the APM App. | `xpack.apm.enabled` | Set to `false` to disable the APM app. Defaults to `true`. +| `xpack.apm.serviceMapFingerprintBucketSize` + | Maximum number of unique transaction combinations sampled for generating service map focused on a specific service. Defaults to `100`. + +| `xpack.apm.serviceMapFingerprintGlobalBucketSize` + | Maximum number of unique transaction combinations sampled for generating the global service map. Defaults to `100`. + | `xpack.apm.ui.enabled` {ess-icon} | Set to `false` to hide the APM app from the main menu. Defaults to `true`. diff --git a/docs/setup/install/deb-init.asciidoc b/docs/setup/install/deb-init.asciidoc deleted file mode 100644 index 6e21b8f97cf7e..0000000000000 --- a/docs/setup/install/deb-init.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -==== Run {kib} with SysV `init` - -Use the `update-rc.d` command to configure Kibana to start automatically -when the system boots up: - -[source,sh] --------------------------------------------------- -sudo update-rc.d kibana defaults 95 10 --------------------------------------------------- - -You can start and stop Kibana using the `service` command: - -[source,sh] --------------------------------------------- -sudo -i service kibana start -sudo -i service kibana stop --------------------------------------------- - -If Kibana fails to start for any reason, it will print the reason for -failure to `STDOUT`. Log files can be found in `/var/log/kibana/`. diff --git a/docs/setup/install/deb.asciidoc b/docs/setup/install/deb.asciidoc index 234c02cee0be1..c830faf1432d7 100644 --- a/docs/setup/install/deb.asciidoc +++ b/docs/setup/install/deb.asciidoc @@ -160,15 +160,7 @@ https://artifacts.elastic.co/downloads/kibana/kibana-oss-{version}-amd64.deb endif::[] -==== SysV `init` vs `systemd` - -include::init-systemd.asciidoc[] - -[[deb-running-init]] -include::deb-init.asciidoc[] - [[deb-running-systemd]] - include::systemd.asciidoc[] [[deb-configuring]] diff --git a/docs/setup/install/rpm-init.asciidoc b/docs/setup/install/rpm-init.asciidoc deleted file mode 100644 index 08282635a014f..0000000000000 --- a/docs/setup/install/rpm-init.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -==== Run {kib} with SysV `init` - -Use the `chkconfig` command to configure Kibana to start automatically -when the system boots up: - -[source,sh] --------------------------------------------------- -sudo chkconfig --add kibana --------------------------------------------------- - -You can start and stop Kibana using the `service` command: - -[source,sh] --------------------------------------------- -sudo -i service kibana start -sudo -i service kibana stop --------------------------------------------- - -If Kibana fails to start for any reason, it will print the reason for -failure to `STDOUT`. Log files can be found in `/var/log/kibana/`. diff --git a/docs/setup/install/rpm.asciidoc b/docs/setup/install/rpm.asciidoc index 1153353aa9a0f..0b63684808d7d 100644 --- a/docs/setup/install/rpm.asciidoc +++ b/docs/setup/install/rpm.asciidoc @@ -153,13 +153,6 @@ https://artifacts.elastic.co/downloads/kibana/kibana-oss-{version}-x86_64.rpm endif::[] -==== SysV `init` vs `systemd` - -include::init-systemd.asciidoc[] - -[[rpm-running-init]] -include::rpm-init.asciidoc[] - [[rpm-running-systemd]] include::systemd.asciidoc[] diff --git a/docs/setup/start-stop.asciidoc b/docs/setup/start-stop.asciidoc index 198bc76bbb400..8952cd3a23cd3 100644 --- a/docs/setup/start-stop.asciidoc +++ b/docs/setup/start-stop.asciidoc @@ -25,25 +25,8 @@ stop and start {kib} from the command line. include::install/windows-running.asciidoc[] [float] -[[start-stop-deb]] -=== Debian packages - -include::install/init-systemd.asciidoc[] - -[float] -include::install/deb-init.asciidoc[] - -[float] -include::install/systemd.asciidoc[] - -[float] -[[start-stop-rpm]] -=== RPM packages - -include::install/init-systemd.asciidoc[] - -[float] -include::install/rpm-init.asciidoc[] +[[start-stop-deb-rpm]] +=== Debian and RPM packages [float] include::install/systemd.asciidoc[] diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index 6d69b6921b612..92cd6e9ead5a1 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -4,29 +4,24 @@ Depending on the {kib} version you're upgrading from, the upgrade process to 7.0 varies. -NOTE: {kib} upgrades automatically when starting a new version, as described in -<>. -Although you do not need to manually back up {kib} before upgrading, we recommend -that you have a backup on hand. You can use -<> to back up {kib} -data by targeting `.kibana*` indices. If you are using the Reporting plugin, -you can also target `.reporting*` indices. - [float] [[upgrade-before-you-begin]] === Before you begin +WARNING: {kib} automatically runs upgrade migrations when required. To roll back to an earlier version in case of an upgrade failure, you **must** have a backup snapshot available. Use <> to back up {kib} data by targeting the `.kibana*` indices. For more information see <>. + Before you upgrade {kib}: * Consult the <>. +* Back up your data with <>. To roll back to an earlier version, you **must** have a snapshot of the `.kibana*` indices. +* Although not a requirement for rollbacks, we recommend taking a snapshot of all {kib} indices created by the plugins you use such as the `.reporting*` indices created by the reporting plugin. * Before you upgrade production servers, test the upgrades in a dev environment. -* Back up your data with {es} {ref}/modules-snapshots.html[snapshots]. - To roll back to an earlier version, you **must** have a backup of your data. +* See <> for common reasons upgrades fail and how to prevent these. * If you are using custom plugins, check that a compatible version is available. -* Shut down all {kib} nodes. Running more than one {kib} version against the - same Elasticseach index is unsupported. If you upgrade while older {kib} nodes are - running, the upgrade can fail. +* Shut down all {kib} instances. Running more than one {kib} version against + the same Elasticseach index is unsupported. Upgrading while older {kib} + instances are running can cause data loss or upgrade failures. To identify the changes you need to make to upgrade, and to enable you to perform an Elasticsearch rolling upgrade with no downtime, you must upgrade to diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index cbe56b9e65894..74d097164c4a7 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -1,54 +1,127 @@ [[upgrade-migrations]] -=== Migrate saved objects +=== Upgrade migrations -Every time {kib} is upgraded it checks to see if all saved objects, such as dashboards, visualizations, and index patterns, are compatible with the new version. If any objects need to be updated, then the automatic saved object migration process is kicked off. +Every time {kib} is upgraded it checks to see if all saved objects, such as dashboards, visualizations, and index patterns, are compatible with the new version. If any saved objects need to be updated, then the automatic saved object migration process is kicked off. NOTE: 6.7 includes an https://www.elastic.co/guide/en/kibana/6.7/upgrade-assistant.html[Upgrade Assistant] to help you prepare for your upgrade to 7.0. To access the assistant, go to *Management > 7.0 Upgrade Assistant*. +WARNING: The following instructions assumes {kib} is using the default index names. If the `kibana.index` or `xpack.tasks.index` configuration settings were changed these instructions will have to be adapted accordingly. + [float] [[upgrade-migrations-process]] -==== How the process works +==== Background -Saved objects are stored in an index named `.kibana_N`, where `N` is a number that increments over time as {kib} is upgraded. The index alias `.kibana` points to the latest up-to-date index for a given install. +Saved objects are stored in two indices: -NOTE: Prior to 6.5.0, saved objects were stored directly in an index named `.kibana`, so the first time you upgrade to {kib} version 6.5+, {kib} will migrate into `.kibana_1` and set `.kibana` up as an index alias. +* `.kibana_N`, or if set, the `kibana.index` configuration setting +* `.kibana_task_manager_N`, or if set, the `xpack.tasks.index` configuration setting + +For each of these indices, `N` is a number that increments every time {kib} runs an upgrade migration on that index. The index aliases `.kibana` and `.kibana_task_manager` point to the most up-to-date index. While {kib} is starting up and before serving any HTTP traffic, it checks to see if any internal mapping changes or data transformations for existing saved objects are required. -When changes are necessary, a new incremental `.kibana_N` index is created with updated mappings, then the saved objects are loaded in batches from the existing index, transformed to whatever extent necessary, and added to this new index. +When changes are necessary, a new migration is started. To ensure that only one {kib} instance performs the migration, each instance will attempt to obtain a migration lock by creating a new `.kibana_N+1` index. The instance that succeeds in creating the index will then read batches of documents from the existing index, migrate them, and write them to the new index. Once the objects are migrated, the lock is released by pointing the `.kibana` index alias the new upgraded `.kibana_N+1` index. + +Instances that failed to acquire a lock will log `Another Kibana instance appears to be migrating the index. Waiting for that migration to complete`. The instance will then wait until `.kibana` points to an upgraded index before starting up and serving HTTP traffic. -Once the objects are migrated, the `.kibana` index alias is updated to point to the new index, and {kib} finishes starting up and serving HTTP traffic. +NOTE: Prior to 6.5.0, saved objects were stored directly in an index named `.kibana`. After upgrading to version 6.5+, {kib} will migrate this index into `.kibana_N` and set `.kibana` up as an index alias. + +Prior to 7.4.0, task manager tasks were stored directly in an index name `.kibana_task_manager`. After upgrading to version 7.4+, {kib} will migrate this index into `.kibana_task_manager_N` and set `.kibana_task_manager` up as an index alias. [float] -[[upgrade-migrations-old-indices]] -==== Handling old `.kibana` indices +[[preventing-migration-failures]] +==== Preventing migration failures +This section highlights common causes of {kib} upgrade failures and how to prevent them. + +[float] +===== Corrupt saved objects +We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. + +[float] +===== User defined index templates that causes new `.kibana*` indices to have incompatible settings or mappings +Matching index templates which specify `settings.refresh_interval` or `mappings` are known to interfere with {kib} upgrades. + +Prevention: narrow down the index patterns of any user-defined index templates to ensure that these won't apply to new `.kibana*` indices. -After migrations have run, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, etc). {kib} only uses the index that the `.kibana` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. +Note: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` and index pattern `.kibana`. This index template will not interfere and does not need to be changed or removed. + +[float] +===== An unhealthy {es} cluster +Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensure that your cluster has: + + * enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices + * sufficient heap size + * a "green" cluster status + +[float] +===== Running different versions of {kib} connected to the same {es} index +Kibana does not support rolling upgrades. Stop all {kib} instances before starting a newer version to prevent upgrade failures and data loss. + +[float] +===== Incompatible `xpack.tasks.index` configuration setting +For {kib} < 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. [float] -[[upgrade-migrations-errors]] -==== Handling errors during saved object migrations +[[resolve-migrations-failures]] +==== Resolving migration failures -If {kib} terminates unexpectedly while migrating a saved object index, some additional work may be required in order to get {kib} to re-attempt the migration. +If {kib} terminates unexpectedly while migrating a saved object index, manual intervention is required before {kib} will attempt to perform the migration again. Follow the advice in (preventing migration failures)[preventing-migration-failures] before retrying a migration upgrade. -For example, if the `.kibana` alias is pointing to `.kibana_4`, and there is a `.kibana_5` index in {es}, the `.kibana_5` index will need to be deleted. {kib} will never attempt to overwrite an existing index. +As mentioned above, {kib} will create a migration lock for each index that requires a migration by creating a new `.kibana_N+1` index. For example: if the `.kibana_task_manager` alias is pointing to `.kibana_task_manager_5` then the first {kib} that succeeds in creating `.kibana_task_manager_6` will obtain the lock to start migrations. + +However, if the instance that obtained the lock fails to migrate the index, all other {kib} instances will be blocked from performing this migration. This includes the instance that originally obtained the lock, it will be blocked from retrying the migration even when restarted. [float] -[[upgrade-migrations-multiple-instances]] -==== Support for multiple {kib} instances +===== Retry a migration by restoring a backup snapshot: + +1. Before proceeding ensure that you have a recent and successful backup snapshot of all `.kibana*` indices. +2. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. +3. Delete all saved object indices with `DELETE /.kibana*` +4. Restore the `.kibana* indices and their aliases from the backup snapshot. See {es} {ref}/modules-snapshots.html[snapshots] +5. Start up all {kib} instances to retry the upgrade migration. -If you're running multiple {kib} instances for a single index behind a load balancer, it's important that you stop all instances before upgrading, so you do not have multiple different versions of {kib} trying to perform saved object migrations. +[float] +===== (Not recommended) Retry a migration without a backup snapshot: -The first instance that triggers saved object migrations will run the entire process. Any other instances started up while a migration is running will log a message and then wait until saved object migration has completed before they start serving HTTP traffic. +1. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. +2. Identify any migration locks by comparing the output of `GET /_cat/aliases` and `GET /_cat/indices`. If e.g. `.kibana` is pointing to `.kibana_4` and there is a `.kibana_5` index, the `.kibana_5` index will act like a migration lock blocking further attempts. Be sure to check both the `.kibana` and `.kibana_task_manager` aliases and their indices. +3. Remove any migration locks e.g. `DELETE /.kibana_5`. +4. Start up all {kib} instances. [float] [[upgrade-migrations-rolling-back]] ==== Rolling back to a previous version of {kib} -When rolling {kib} back to a previous version, point the `.kibana` alias to -the appropriate {kib} index. When you have the previous version running again, -delete the more recent `.kibana_N` index or indices so that future upgrades are -based on the current {kib} index. You must restart {kib} to re-trigger the migration. +If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify the root cause. + +WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. + +Any changes made after an upgrade will be lost when rolling back to a previous version. + +In order to rollback after a failed upgrade migration, the saved object indices might also have to be rolled back to be compatible with the previous {kibana} version. + +[float] +===== Rollback by restoring a backup snapshot: + +1. Before proceeding ensure that you have a recent and successful backup snapshot of all `.kibana*` indices. +2. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. +3. Delete all saved object indices with `DELETE /.kibana*` +4. Restore the `.kibana* indices and their aliases from the backup snapshot. See {es} {ref}/modules-snapshots.html[snapshots] +5. Start up all {kib} instances on the older version you wish to rollback to. + +[float] +===== (Not recommended) Rollback without a backup snapshot: + +WARNING: {kib} does not run a migration for every saved object index on every upgrade. A {kib} version upgrade can cause no migrations, migrate only the `.kibana` or the `.kibana_task_manager` index or both. Carefully read the logs to ensure that you're only deleting indices created by a later version of {kib} to avoid data loss. + +1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. +2. Create a backup snapshot of the `.kibana*` indices. +3. Use the logs from the upgraded instances to identify which indices {kib} attempted to upgrade. The server logs will contain an entry like `[savedobjects-service] Creating index .kibana_4.` and/or `[savedobjects-service] Creating index .kibana_task_manager_2.` If no indices were created after upgrading {kib} then no further action is required to perform a rollback, skip ahead to step (5). If you're running multiple {kib} instances, be sure to inspect all instances' logs. +4. Delete each of the indices identified in step (2). e.g. `DELETE /.kibana_task_manager_2` +5. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +6. Start up {kib} on the older version you wish to rollback to. + +[float] +[[upgrade-migrations-old-indices]] +==== Handling old `.kibana_N` indices -WARNING: Rolling back to a previous {kib} version can result in saved object data loss if you had successfully upgraded and made changes to saved objects before rolling back. +After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, etc). {kib} only uses the index that the `.kibana` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file diff --git a/docs/setup/upgrade/upgrade-standard.asciidoc b/docs/setup/upgrade/upgrade-standard.asciidoc index df38427881d65..b27bb8867e624 100644 --- a/docs/setup/upgrade/upgrade-standard.asciidoc +++ b/docs/setup/upgrade/upgrade-standard.asciidoc @@ -12,11 +12,20 @@ If you've saved and/or exported objects in {kib} that rely on the necessary remediation steps as per those instructions. =========================================== +[float] +==== Upgrading multiple {kib} instances + +WARNING: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading. + +Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause upgrade migration failures and data loss. This is because acknowledged writes from the older instances could be written into the _old_ index while the migration is in progress. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version. + +The first instance that triggers saved object migrations will run the entire process. Any other instances started up while a migration is running will log a message and then wait until saved object migrations has completed before they start serving HTTP traffic. + [float] ==== Upgrade using a `deb` or `rpm` package . Stop the existing {kib} process using the appropriate command for your - system. + system. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. . Use `rpm` or `dpkg` to install the new package. All files should be placed in their proper locations and config files should not be overwritten. + @@ -43,8 +52,7 @@ otherwise {kib} will fail to start. don't overwrite the `config` or `data` directories. + + -- -IMPORTANT: If you use {monitor-features}, you must re-use the data directory when you -upgrade {kib}. Otherwise, the {kib} instance is assigned a new persistent UUID +IMPORTANT: If you use {monitor-features}, you must re-use the data directory when you upgrade {kib}. Otherwise, the {kib} instance is assigned a new persistent UUID and becomes a new instance in the monitoring data. -- @@ -57,5 +65,5 @@ and becomes a new instance in the monitoring data. . Install the appropriate versions of all your plugins for your new installation using the `kibana-plugin` script. Check out the <> documentation for more information. -. Stop the old {kib} process. +. Stop the old {kib} process. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. . Start the new {kib} process. diff --git a/docs/spaces/images/edit-space-feature-visibility.png b/docs/spaces/images/edit-space-feature-visibility.png index bc983bde2679c..3a74c21fe2635 100644 Binary files a/docs/spaces/images/edit-space-feature-visibility.png and b/docs/spaces/images/edit-space-feature-visibility.png differ diff --git a/docs/spaces/images/edit-space.png b/docs/spaces/images/edit-space.png index 68ffea23c4ac4..c4565a3ba0f4a 100644 Binary files a/docs/spaces/images/edit-space.png and b/docs/spaces/images/edit-space.png differ diff --git a/docs/spaces/images/space-selector.png b/docs/spaces/images/space-selector.png index 908c5360acd39..b2576cbc9acc4 100644 Binary files a/docs/spaces/images/space-selector.png and b/docs/spaces/images/space-selector.png differ diff --git a/docs/spaces/images/spaces-roles.png b/docs/spaces/images/spaces-roles.png old mode 100755 new mode 100644 index 2ecbfed6017c4..407926895daa8 Binary files a/docs/spaces/images/spaces-roles.png and b/docs/spaces/images/spaces-roles.png differ diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc index 1bc781e1dda49..81f3945779503 100644 --- a/docs/spaces/index.asciidoc +++ b/docs/spaces/index.asciidoc @@ -25,6 +25,11 @@ Kibana supports spaces in several ways. You can: * <> * <> +[float] +==== Required permissions + +The `kibana_admin` role or equivilent is required to manage **Spaces**. + [float] [[spaces-managing]] === View, create, and delete spaces diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index aa5b0ece08db7..91f149d5cdb3c 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -112,7 +112,7 @@ You can even choose which features to enable within each space. Don’t need Machine learning in your “Executive” space? Simply turn it off. [role="screenshot"] -image::images/intro-spaces.jpg[] +image::images/intro-spaces.png[Space selector screen] You can take this all one step further with Kibana’s security features, and control which users have access to each space. {kib} allows for fine-grained diff --git a/docs/user/introduction/images/intro-spaces.jpg b/docs/user/introduction/images/intro-spaces.jpg deleted file mode 100755 index 7569dfc16b4f7..0000000000000 Binary files a/docs/user/introduction/images/intro-spaces.jpg and /dev/null differ diff --git a/docs/user/introduction/images/intro-spaces.png b/docs/user/introduction/images/intro-spaces.png index 6f3212cbde26e..b2576cbc9acc4 100644 Binary files a/docs/user/introduction/images/intro-spaces.png and b/docs/user/introduction/images/intro-spaces.png differ diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index c371aa695c475..ee85819b4fd98 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -6,6 +6,10 @@ *Stack Management* is home to UIs for managing all things Elastic Stack— indices, clusters, licenses, UI settings, index patterns, spaces, and more. + +Access to individual features is governed by {es} and {kib} privileges. +Consult your administrator if you do not have the appropriate access. + [float] [[manage-ingest]] == Ingest diff --git a/docs/user/monitoring/beats-details.asciidoc b/docs/user/monitoring/beats-details.asciidoc index 3d7a726d2f8a2..dbd9aecd04768 100644 --- a/docs/user/monitoring/beats-details.asciidoc +++ b/docs/user/monitoring/beats-details.asciidoc @@ -9,7 +9,7 @@ If you are monitoring Beats, the *Stack Monitoring* page in {kib} contains a panel for Beats in the cluster overview. [role="screenshot"] -image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="images/monitoring-beats.jpg"] +image::user/monitoring/images/monitoring-beats.jpg["Monitoring Beats",link="user/monitoring/images/monitoring-beats.jpg"] To view an overview of the Beats data in the cluster, click *Overview*. The overview page has a section for activity in the last day, which is a real-time @@ -24,7 +24,7 @@ cluster. All columns are sortable. Clicking a Beat name takes you to the detail page. For example: [role="screenshot"] -image::user/monitoring/images/monitoring-beats-detail.jpg["Monitoring details for Filebeat",link="images/monitoring-beats-detail.jpg"] +image::user/monitoring/images/monitoring-beats-detail.jpg["Monitoring details for Filebeat",link="user/monitoring/images/monitoring-beats-detail.jpg"] The detail page contains a summary bar and charts. There are more charts on this page than the overview page and they are specific to a single Beat instance. diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 150004b3ad691..af5fd34b3b350 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -12,7 +12,12 @@ NOTE: When running multiple tenants of {kib} by changing the `kibana.index` in y [[xpack-kibana-role-management]] === {kib} role management -To create a role that grants {kib} privileges, open the main menu, click *Stack Management > Roles*, then click *Create role*. +To create a role that grants {kib} privileges, open the menu, then click *Stack Management > Roles* and click **Create role**. + +[float] +==== Required permissions + +The `manage_security` cluster privilege is required to access role management. [[adding_kibana_privileges]] ==== Adding {kib} privileges diff --git a/docs/user/security/images/add-space-privileges.png b/docs/user/security/images/add-space-privileges.png old mode 100755 new mode 100644 index 7739332c33b60..d2fcbe76c1a06 Binary files a/docs/user/security/images/add-space-privileges.png and b/docs/user/security/images/add-space-privileges.png differ diff --git a/docs/user/security/images/assign_base_privilege.png b/docs/user/security/images/assign_base_privilege.png index 34e2bcf81d618..93bed0de05555 100644 Binary files a/docs/user/security/images/assign_base_privilege.png and b/docs/user/security/images/assign_base_privilege.png differ diff --git a/docs/user/security/images/assign_feature_privilege.png b/docs/user/security/images/assign_feature_privilege.png index c9449f6390253..d42ec208325a3 100644 Binary files a/docs/user/security/images/assign_feature_privilege.png and b/docs/user/security/images/assign_feature_privilege.png differ diff --git a/docs/user/security/images/privilege-example-1.png b/docs/user/security/images/privilege-example-1.png old mode 100755 new mode 100644 index 68ba716437240..b8fb4d15b8f77 Binary files a/docs/user/security/images/privilege-example-1.png and b/docs/user/security/images/privilege-example-1.png differ diff --git a/docs/user/security/images/role-space-visualization.png b/docs/user/security/images/role-space-visualization.png index 746af89c66e85..0de94d81065fe 100644 Binary files a/docs/user/security/images/role-space-visualization.png and b/docs/user/security/images/role-space-visualization.png differ diff --git a/docs/user/security/images/view-privilege-summary.png b/docs/user/security/images/view-privilege-summary.png old mode 100755 new mode 100644 index d93d55c93fd12..7d2f3018d7de9 Binary files a/docs/user/security/images/view-privilege-summary.png and b/docs/user/security/images/view-privilege-summary.png differ diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc index b5ab57d8f525a..18ace452ce00c 100644 --- a/docs/user/security/index.asciidoc +++ b/docs/user/security/index.asciidoc @@ -10,6 +10,12 @@ auditing. For more information, see {ref}/secure-cluster.html[Secure a cluster] and <>. +[float] +=== Required permissions + +The `manage_security` cluster privilege is required to access all Security features. + + [float] === Users diff --git a/docs/user/security/role-mappings/index.asciidoc b/docs/user/security/role-mappings/index.asciidoc index 3f9a17e98d77f..ca3ca9a686892 100644 --- a/docs/user/security/role-mappings/index.asciidoc +++ b/docs/user/security/role-mappings/index.asciidoc @@ -19,6 +19,11 @@ With *Role mappings*, you can: [role="screenshot"] image:user/security/role-mappings/images/role-mappings-grid.png["Role mappings"] +[float] +==== Required permissions + +The `manage_security` cluster privilege is required to manage Role Mappings. + [float] === Create a role mapping diff --git a/examples/bfetch_explorer/README.md b/examples/bfetch_explorer/README.md new file mode 100644 index 0000000000000..33723e7cabe07 --- /dev/null +++ b/examples/bfetch_explorer/README.md @@ -0,0 +1,11 @@ +## bfetch explorer + +bfetch is a service that allows you to batch HTTP requests and stream responses +back. + +This example app demonstrates: + - How you can create a streaming response route and consume it from the + client + - How you can create a batch processing route and consume it from the client + +To run this example, use the command `yarn start --run-examples`. diff --git a/examples/embeddable_examples/README.md b/examples/embeddable_examples/README.md new file mode 100644 index 0000000000000..d05c315d31817 --- /dev/null +++ b/examples/embeddable_examples/README.md @@ -0,0 +1,4 @@ +## Embeddable examples + +This example plugin exists to support the `embeddable_explorer` app. + diff --git a/examples/embeddable_explorer/README.md b/examples/embeddable_explorer/README.md new file mode 100644 index 0000000000000..0425790f07487 --- /dev/null +++ b/examples/embeddable_explorer/README.md @@ -0,0 +1,10 @@ +## Embeddable explorer + +This example app shows how to: + - Create a basic "hello world" embeddable + - Create embeddables that accept inputs and use an EmbeddableRenderer + - Nest embeddables inside a container + - Dynamically add children to embeddable containers + - Work with the EmbeddablePanel component + +To run this example, use the command `yarn start --run-examples`. diff --git a/examples/embeddable_explorer/tsconfig.json b/examples/embeddable_explorer/tsconfig.json index 86b35c5e4943f..4baebebcea42e 100644 --- a/examples/embeddable_explorer/tsconfig.json +++ b/examples/embeddable_explorer/tsconfig.json @@ -9,10 +9,11 @@ "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", - "../../typings/**/*", + "../../typings/**/*" ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/inspector/tsconfig.json" } ] } diff --git a/examples/state_containers_examples/README.md b/examples/state_containers_examples/README.md new file mode 100644 index 0000000000000..c4c6642789bd9 --- /dev/null +++ b/examples/state_containers_examples/README.md @@ -0,0 +1,8 @@ +## State containers examples + +This example app shows how to: + - Use state containers to manage your application state + - Integrate with browser history and hash history routing + - Sync your state container with the URL + +To run this example, use the command `yarn start --run-examples`. diff --git a/packages/kbn-apm-config-loader/src/config.test.ts b/packages/kbn-apm-config-loader/src/config.test.ts index 83438215716ac..8adc4d84635af 100644 --- a/packages/kbn-apm-config-loader/src/config.test.ts +++ b/packages/kbn-apm-config-loader/src/config.test.ts @@ -28,6 +28,8 @@ import { import { ApmConfiguration } from './config'; +const initialEnv = { ...process.env }; + describe('ApmConfiguration', () => { beforeEach(() => { packageMock.raw = { @@ -39,6 +41,7 @@ describe('ApmConfiguration', () => { }); afterEach(() => { + process.env = { ...initialEnv }; resetAllMocks(); }); @@ -90,13 +93,16 @@ describe('ApmConfiguration', () => { let config = new ApmConfiguration(mockedRootDir, {}, false); expect(config.getConfig('serviceName')).toEqual( expect.objectContaining({ - serverUrl: expect.any(String), - secretToken: expect.any(String), + breakdownMetrics: true, }) ); config = new ApmConfiguration(mockedRootDir, {}, true); - expect(Object.keys(config.getConfig('serviceName'))).not.toContain('serverUrl'); + expect(config.getConfig('serviceName')).toEqual( + expect.objectContaining({ + breakdownMetrics: false, + }) + ); }); it('loads the configuration from the kibana config file', () => { @@ -156,4 +162,32 @@ describe('ApmConfiguration', () => { }) ); }); + + it('correctly sets environment', () => { + delete process.env.ELASTIC_APM_ENVIRONMENT; + delete process.env.NODE_ENV; + + let config = new ApmConfiguration(mockedRootDir, {}, false); + expect(config.getConfig('serviceName')).toEqual( + expect.objectContaining({ + environment: 'development', + }) + ); + + process.env.NODE_ENV = 'production'; + config = new ApmConfiguration(mockedRootDir, {}, false); + expect(config.getConfig('serviceName')).toEqual( + expect.objectContaining({ + environment: 'production', + }) + ); + + process.env.ELASTIC_APM_ENVIRONMENT = 'ci'; + config = new ApmConfiguration(mockedRootDir, {}, false); + expect(config.getConfig('serviceName')).toEqual( + expect.objectContaining({ + environment: 'ci', + }) + ); + }); }); diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index 897e7fd7ca610..a611e205ec83a 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -26,32 +26,26 @@ import { readFileSync } from 'fs'; import { ApmAgentConfig } from './types'; const getDefaultConfig = (isDistributable: boolean): ApmAgentConfig => { - if (isDistributable) { - return { - active: false, - globalLabels: {}, - // Do not use a centralized controlled config - centralConfig: false, - // Capture all exceptions that are not caught - logUncaughtExceptions: true, - // Can be performance intensive, disabling by default - breakdownMetrics: false, - }; - } - + // https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html return { - active: false, - serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', + active: process.env.ELASTIC_APM_ACTIVE || false, + environment: process.env.ELASTIC_APM_ENVIRONMENT || process.env.NODE_ENV || 'development', + + serverUrl: 'https://b1e3b4b4233e44cdad468c127d0af8d8.apm.europe-west1.gcp.cloud.es.io:443', + // The secretToken below is intended to be hardcoded in this file even though // it makes it public. This is not a security/privacy issue. Normally we'd // instead disable the need for a secretToken in the APM Server config where // the data is transmitted to, but due to how it's being hosted, it's easier, // for now, to simply leave it in. - secretToken: 'R0Gjg46pE9K9wGestd', + secretToken: '2OyjjaI6RVkzx2O5CV', + + logUncaughtExceptions: true, globalLabels: {}, - breakdownMetrics: true, centralConfig: false, - logUncaughtExceptions: true, + + // Can be performance intensive, disabling by default + breakdownMetrics: isDistributable ? false : true, }; }; @@ -84,7 +78,8 @@ export class ApmConfiguration { getDefaultConfig(this.isDistributable), this.getConfigFromKibanaConfig(), this.getDevConfig(), - this.getDistConfig() + this.getDistConfig(), + this.getCIConfig() ); const rev = this.getGitRev(); @@ -146,6 +141,21 @@ export class ApmConfiguration { }; } + private getCIConfig(): ApmAgentConfig { + if (process.env.ELASTIC_APM_ENVIRONMENT !== 'ci') { + return {}; + } + + return { + globalLabels: { + branch: process.env.ghprbSourceBranch || '', + targetBranch: process.env.ghprbTargetBranch || '', + ciJobName: process.env.JOB_NAME || '', + ciBuildNumber: process.env.BUILD_NUMBER || '', + }, + }; + } + private getGitRev() { if (this.isDistributable) { return this.pkgBuild.sha; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index c660d37222504..3f9fdb164e759 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -14,7 +14,7 @@ pageLoadAssetSize: dashboard: 374194 dashboardEnhanced: 65646 dashboardMode: 22716 - data: 1287839 + data: 1317839 dataEnhanced: 50420 devTools: 38637 discover: 105145 diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 28b3e37380b4e..976ffe1f5b1b5 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -146,7 +146,11 @@ run( await lastValueFrom(update$.pipe(logOptimizerState(log, config))); if (updateLimits) { - updateBundleLimits(log, config); + updateBundleLimits({ + log, + config, + dropMissing: !(focus || filter), + }); } }, { diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index cb5bb1e8fc529..ddb19c8cdc3d7 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -108,4 +108,4 @@ exports[`prepares assets for distribution: baz bundle 1`] = ` exports[`prepares assets for distribution: foo async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],{3:function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}}]);"`; -exports[`prepares assets for distribution: foo bundle 1`] = `"(function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i = {}; + const pageLoadAssetSize: NonNullable = dropMissing + ? {} + : config.limits.pageLoadAssetSize ?? {}; for (const metric of metrics.sort((a, b) => a.id.localeCompare(b.id))) { if (metric.group === 'page load bundle size') { diff --git a/packages/kbn-telemetry-tools/GUIDELINE.md b/packages/kbn-telemetry-tools/GUIDELINE.md index e7d09babbf9e2..a22196bb5dc74 100644 --- a/packages/kbn-telemetry-tools/GUIDELINE.md +++ b/packages/kbn-telemetry-tools/GUIDELINE.md @@ -148,14 +148,7 @@ usageCollection.makeUsageCollector({ Any field property in the schema accepts a `type` field. By default the type is `object` which accepts nested properties under it. Currently we accept the following property types: ``` -AllowedSchemaTypes = - | 'keyword' - | 'text' - | 'number' - | 'boolean' - | 'long' - | 'date' - | 'float'; +'long', 'integer', 'short', 'byte', 'double', 'float', 'keyword', 'text', 'boolean', 'date' ``` diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json index 51e5df9bf7dc0..a385cd6798365 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json @@ -8,16 +8,16 @@ "my_index_signature_prop": { "properties": { "avg": { - "type": "number" + "type": "float" }, "count": { - "type": "number" + "type": "long" }, "max": { - "type": "number" + "type": "long" }, "min": { - "type": "number" + "type": "long" } } }, @@ -27,7 +27,7 @@ "my_objects": { "properties": { "total": { - "type": "number" + "type": "long" }, "type": { "type": "boolean" @@ -39,7 +39,7 @@ "items": { "properties": { "total": { - "type": "number" + "type": "long" }, "type": { "type": "boolean" diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts index 83866a2b6afec..109fc045b6ee0 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts @@ -28,7 +28,7 @@ export const parsedIndexedInterfaceWithNoMatchingSchema: ParsedUsageCollection = value: { something: { count_1: { - type: 'number', + type: 'long', }, }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts index 833344fa368b0..4a1a622e23f36 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_schema_defined_with_spreads_collector.ts @@ -34,7 +34,7 @@ export const parsedSchemaDefinedWithSpreadsCollector: ParsedUsageCollection = [ }, my_objects: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean', diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts index acf984b7d10ee..ef6227cf35c37 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts @@ -34,21 +34,21 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ }, my_index_signature_prop: { avg: { - type: 'number', + type: 'float', }, count: { - type: 'number', + type: 'long', }, max: { - type: 'number', + type: 'long', }, min: { - type: 'number', + type: 'long', }, }, my_objects: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean', @@ -58,7 +58,7 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ type: 'array', items: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean' }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap index 4725be77533af..fe589be7993d0 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap @@ -176,7 +176,7 @@ Array [ }, "my_objects": Object { "total": Object { - "type": "number", + "type": "long", }, "type": Object { "type": "boolean", @@ -248,7 +248,7 @@ Array [ "my_array": Object { "items": Object { "total": Object { - "type": "number", + "type": "long", }, "type": Object { "type": "boolean", @@ -258,21 +258,21 @@ Array [ }, "my_index_signature_prop": Object { "avg": Object { - "type": "number", + "type": "float", }, "count": Object { - "type": "number", + "type": "long", }, "max": Object { - "type": "number", + "type": "long", }, "min": Object { - "type": "number", + "type": "long", }, }, "my_objects": Object { "total": Object { - "type": "number", + "type": "long", }, "type": Object { "type": "boolean", diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts index a101210185a63..b6ea9d49cf6d0 100644 --- a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts @@ -44,7 +44,7 @@ describe('checkMatchingMapping', () => { it('returns diff on mismatching parsedCollections and stored mapping', async () => { const mockSchema = await parseJsonFile('mock_schema.json'); const malformedParsedCollector = cloneDeep(parsedWorkingCollector); - const fieldMapping = { type: 'number' }; + const fieldMapping = { type: 'long' }; malformedParsedCollector[1].schema.value.flat = fieldMapping; const diffs = checkMatchingMapping([malformedParsedCollector], mockSchema); @@ -61,9 +61,9 @@ describe('checkMatchingMapping', () => { const mockSchema = await parseJsonFile('mock_schema.json'); const malformedParsedCollector = cloneDeep(parsedWorkingCollector); const collectorName = 'New Collector in town!'; - const collectorMapping = { some_usage: { type: 'number' } }; + const collectorMapping = { some_usage: { type: 'long' } }; malformedParsedCollector[1].collectorName = collectorName; - malformedParsedCollector[1].schema.value = { some_usage: { type: 'number' } }; + malformedParsedCollector[1].schema.value = { some_usage: { type: 'long' } }; const diffs = checkMatchingMapping([malformedParsedCollector], mockSchema); expect(diffs).toEqual({ diff --git a/packages/kbn-telemetry-tools/src/tools/manage_schema.ts b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts index 7721492fdb691..e2bfca34a6487 100644 --- a/packages/kbn-telemetry-tools/src/tools/manage_schema.ts +++ b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts @@ -19,14 +19,9 @@ import { ParsedUsageCollection } from './ts_parser'; -export type AllowedSchemaTypes = - | 'keyword' - | 'text' - | 'number' - | 'boolean' - | 'long' - | 'date' - | 'float'; +export type AllowedSchemaNumberTypes = 'long' | 'integer' | 'short' | 'byte' | 'double' | 'float'; + +export type AllowedSchemaTypes = AllowedSchemaNumberTypes | 'keyword' | 'text' | 'boolean' | 'date'; export function compatibleSchemaTypes(type: AllowedSchemaTypes | 'array') { switch (type) { @@ -36,9 +31,12 @@ export function compatibleSchemaTypes(type: AllowedSchemaTypes | 'array') { return 'string'; case 'boolean': return 'boolean'; - case 'number': - case 'float': case 'long': + case 'integer': + case 'short': + case 'byte': + case 'double': + case 'float': return 'number'; case 'array': return 'array'; diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index d3fb5bdf36194..093cd29ba5507 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -110,3 +110,10 @@ export type MethodKeysOf = { * Returns an object with public methods only. */ export type PublicMethodsOf = Pick>; + +/** + * Makes an object with readonly properties mutable. + */ +export type Writable = { + -readonly [K in keyof T]: T[K]; +}; diff --git a/src/core/typings.ts b/packages/kbn-utility-types/jest/index.ts similarity index 90% rename from src/core/typings.ts rename to packages/kbn-utility-types/jest/index.ts index f271d0b03e0d3..8b3d926b8e8df 100644 --- a/src/core/typings.ts +++ b/packages/kbn-utility-types/jest/index.ts @@ -16,12 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - -type DeeplyMockedKeys = { +export type DeeplyMockedKeys = { [P in keyof T]: T[P] extends (...args: any[]) => any ? jest.MockInstance, Parameters> : DeeplyMockedKeys; } & T; -type MockedKeys = { [P in keyof T]: jest.Mocked }; +export type MockedKeys = { [P in keyof T]: jest.Mocked }; diff --git a/packages/kbn-utility-types/jest/package.json b/packages/kbn-utility-types/jest/package.json new file mode 100644 index 0000000000000..9f92fc5e94528 --- /dev/null +++ b/packages/kbn-utility-types/jest/package.json @@ -0,0 +1,3 @@ +{ + "types": "../target/jest/index.d.ts" +} diff --git a/packages/kbn-utility-types/test-d/method_keys_of.ts b/packages/kbn-utility-types/test-d/method_keys_of.ts new file mode 100644 index 0000000000000..46cb17122f416 --- /dev/null +++ b/packages/kbn-utility-types/test-d/method_keys_of.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { expectType } from 'tsd'; +import { MethodKeysOf } from '../index'; + +class Test { + public name: string = ''; + getName() { + return this.name; + } + // @ts-ignore + private getDoubleName() { + return this.name.repeat(2); + } +} + +expectType>('getName'); diff --git a/packages/kbn-utility-types/test-d/public_methods_of.ts b/packages/kbn-utility-types/test-d/public_methods_of.ts new file mode 100644 index 0000000000000..3f5d442342b15 --- /dev/null +++ b/packages/kbn-utility-types/test-d/public_methods_of.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { expectAssignable, expectNotAssignable } from 'tsd'; +import { PublicMethodsOf } from '../index'; + +class Test { + public name: string = ''; + getName() { + return this.name; + } + // @ts-ignore + private getDoubleName() { + return this.name.repeat(2); + } +} + +expectAssignable>({ + getName() { + return ''; + }, +}); + +expectNotAssignable>({ + getName() { + return 1; + }, +}); + +expectNotAssignable>({ + getDoubleName() { + return 1; + }, +}); diff --git a/packages/kbn-utility-types/test-d/writable.ts b/packages/kbn-utility-types/test-d/writable.ts new file mode 100644 index 0000000000000..0771fa926c4ec --- /dev/null +++ b/packages/kbn-utility-types/test-d/writable.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { expectAssignable } from 'tsd'; +import { Writable } from '../index'; + +type WritableArray = Writable; +expectAssignable(['1']); + +type WritableObject = Writable<{ + readonly name: string; +}>; +expectAssignable({ name: '1' }); diff --git a/packages/kbn-utility-types/tsconfig.json b/packages/kbn-utility-types/tsconfig.json index 79cf423fe78ac..c2d206526e6f4 100644 --- a/packages/kbn-utility-types/tsconfig.json +++ b/packages/kbn-utility-types/tsconfig.json @@ -7,10 +7,11 @@ "stripInternal": true, "declarationMap": true, "types": [ - "node" + "node", + "jest" ] }, - "include": ["index.ts", "test-d/**/*"], + "include": ["index.ts", "jest/**/*", "test-d/**/*"], "exclude": [ "target" ] diff --git a/src/core/public/apm_system.test.ts b/src/core/public/apm_system.test.ts index f88cdd899ef81..be1cb6dc68208 100644 --- a/src/core/public/apm_system.test.ts +++ b/src/core/public/apm_system.test.ts @@ -18,6 +18,7 @@ */ jest.mock('@elastic/apm-rum'); +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { init, apm } from '@elastic/apm-rum'; import { ApmSystem } from './apm_system'; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 9cd96763d2e79..cbcd23615d34c 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -18,6 +18,7 @@ */ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { ChromeBadge, ChromeBrand, ChromeBreadcrumb, ChromeService, InternalChromeStart } from './'; const createStartContractMock = () => { diff --git a/src/core/public/notifications/notifications_service.mock.ts b/src/core/public/notifications/notifications_service.mock.ts index 990ab479d35c3..b69b4604d0788 100644 --- a/src/core/public/notifications/notifications_service.mock.ts +++ b/src/core/public/notifications/notifications_service.mock.ts @@ -17,6 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { MockedKeys } from '@kbn/utility-types/jest'; import { NotificationsService, NotificationsSetup, diff --git a/src/core/public/overlays/overlay_service.mock.ts b/src/core/public/overlays/overlay_service.mock.ts index 66ba36b20b45c..595813972b8bc 100644 --- a/src/core/public/overlays/overlay_service.mock.ts +++ b/src/core/public/overlays/overlay_service.mock.ts @@ -17,6 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { OverlayService, OverlayStart } from './overlay_service'; import { overlayBannersServiceMock } from './banners/banners_service.mock'; import { overlayFlyoutServiceMock } from './flyout/flyout_service.mock'; diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts index bbf04783278f7..4df0b2dfb9d8a 100644 --- a/src/core/server/core_context.mock.ts +++ b/src/core/server/core_context.mock.ts @@ -18,6 +18,7 @@ */ import { REPO_ROOT } from '@kbn/dev-utils'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { CoreContext } from './core_context'; import { Env, IConfigService } from './config'; import { configServiceMock, getEnvOptions } from './config/mocks'; diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index fb2826c787718..bedd0e65c5a83 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -18,6 +18,7 @@ */ import { 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'; diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 6aad232cf42b6..d615e799f383f 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { parse as parseUrl } from 'url'; import { Request } from 'hapi'; import { merge } from 'lodash'; import { Socket } from 'net'; @@ -72,6 +73,7 @@ function createKibanaRequestMock

({ auth = { isAuthenticated: true }, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); + const url = parseUrl(`${path}${queryString ? `?${queryString}` : ''}`); return KibanaRequest.from( createRawRequestMock({ @@ -83,12 +85,7 @@ function createKibanaRequestMock

({ payload: body, path, method, - url: { - path, - pathname: path, - query: queryString, - search: queryString ? `?${queryString}` : queryString, - }, + url, route: { settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteOptions }, }, @@ -121,6 +118,11 @@ interface DeepPartialArray extends Array> {} type DeepPartialObject = { [P in keyof T]+?: DeepPartial }; function createRawRequestMock(customization: DeepPartial = {}) { + const pathname = customization.url?.pathname || '/'; + const path = `${pathname}${customization.url?.search || ''}`; + const url = Object.assign({ pathname, path, href: path }, customization.url); + + // @ts-expect-error _core isn't supposed to be accessed - remove once we upgrade to hapi v18 return merge( {}, { @@ -129,17 +131,21 @@ function createRawRequestMock(customization: DeepPartial = {}) { isAuthenticated: true, }, headers: {}, - path: '/', + path, route: { settings: {} }, - url: { - href: '/', - }, + url, raw: { req: { - url: '/', + url: path, socket: {}, }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }, customization ) as Request; diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 2440f2b1da0bd..d94bce12fb439 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -271,7 +271,7 @@ export class HttpServer { } this.registerOnPreRouting((request, response, toolkit) => { - const oldUrl = request.url.href!; + const oldUrl = request.url.pathname + request.url.search; const newURL = basePathService.remove(oldUrl); const shouldRedirect = newURL !== oldUrl; if (shouldRedirect) { diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 01817b29de8ac..37401a2c24ccd 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -124,7 +124,13 @@ describe('OnPreRouting', () => { const router = createRouter('/'); router.get({ path: '/login', validate: false }, (context, req, res) => { - return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } }); + return res.ok({ + body: { + rewrittenUrl: req.rewrittenUrl + ? `${req.rewrittenUrl.pathname}${req.rewrittenUrl.search}` + : undefined, + }, + }); }); registerOnPreRouting((req, res, t) => t.rewriteUrl('/login')); @@ -143,7 +149,13 @@ describe('OnPreRouting', () => { const router = createRouter('/'); router.get({ path: '/reroute-2', validate: false }, (context, req, res) => { - return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } }); + return res.ok({ + body: { + rewrittenUrl: req.rewrittenUrl + ? `${req.rewrittenUrl.pathname}${req.rewrittenUrl.search}` + : undefined, + }, + }); }); registerOnPreRouting((req, res, t) => t.rewriteUrl('/reroute-1')); @@ -163,7 +175,13 @@ describe('OnPreRouting', () => { const router = createRouter('/'); router.get({ path: '/login', validate: false }, (context, req, res) => { - return res.ok({ body: { rewrittenUrl: req.rewrittenUrl?.path } }); + return res.ok({ + body: { + rewrittenUrl: req.rewrittenUrl + ? `${req.rewrittenUrl.pathname}${req.rewrittenUrl.search}` + : undefined, + }, + }); }); registerOnPreRouting((req, res, t) => t.next()); diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index e19c348511f1a..ad228d9b0bb9d 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -384,7 +384,7 @@ describe('Options', () => { }); describe('idleSocket', () => { - it('should timeout if payload sending has too long of an idle period', async () => { + it.skip('should timeout if payload sending has too long of an idle period', async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); diff --git a/src/core/server/http/lifecycle/on_pre_routing.ts b/src/core/server/http/lifecycle/on_pre_routing.ts index 92ae1f0b7bbdf..e553f113a7cf8 100644 --- a/src/core/server/http/lifecycle/on_pre_routing.ts +++ b/src/core/server/http/lifecycle/on_pre_routing.ts @@ -17,6 +17,7 @@ * under the License. */ +import { URL } from 'url'; import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi'; import { Logger } from '../../logging'; import { @@ -110,10 +111,30 @@ export function adoptToHapiOnRequest(fn: OnPreRoutingHandler, log: Logger) { if (preRoutingResult.isRewriteUrl(result)) { const appState = request.app as KibanaRequestState; - appState.rewrittenUrl = appState.rewrittenUrl ?? request.url; + appState.rewrittenUrl = + // @ts-expect-error request._core isn't supposed to be accessed - remove once we upgrade to hapi v18 + appState.rewrittenUrl ?? new URL(request.url.href!, request._core.info.uri); const { url } = result; - request.setUrl(url); + + // TODO: Remove once we upgrade to Node.js 12! + // + // Warning: The following for-loop took 10 days to write, and is a hack + // to force V8 to make a copy of the string in memory. + // + // The reason why we need this is because of what appears to be a bug + // in V8 that caused some URL paths to not be routed correctly once + // `request.setUrl` was called with the path. + // + // The details can be seen in this discussion on Twitter: + // https://twitter.com/wa7son/status/1319992632366518277 + let urlCopy = ''; + for (let i = 0; i < url.length; i++) { + urlCopy += url[i]; + } + + request.setUrl(urlCopy); + // We should update raw request as well since it can be proxied to the old platform request.raw.req.url = url; return responseToolkit.continue; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 2d0e8d6c1a6ad..561bf742050c3 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Url } from 'url'; +import { URL } from 'url'; import uuid from 'uuid'; import { Request, RouteOptionsApp, ApplicationState } from 'hapi'; import { Observable, fromEvent, merge } from 'rxjs'; @@ -45,7 +45,7 @@ export interface KibanaRouteOptions extends RouteOptionsApp { export interface KibanaRequestState extends ApplicationState { requestId: string; requestUuid: string; - rewrittenUrl?: Url; + rewrittenUrl?: URL; } /** @@ -163,7 +163,7 @@ export class KibanaRequest< */ public readonly uuid: string; /** a WHATWG URL standard object. */ - public readonly url: Url; + public readonly url: URL; /** matched route details */ public readonly route: RecursiveReadonly>; /** @@ -190,7 +190,7 @@ export class KibanaRequest< /** * URL rewritten in onPreRouting request interceptor. */ - public readonly rewrittenUrl?: Url; + public readonly rewrittenUrl?: URL; /** @internal */ protected readonly [requestSymbol]: Request; @@ -212,7 +212,8 @@ export class KibanaRequest< this.uuid = appState?.requestUuid ?? uuid.v4(); this.rewrittenUrl = appState?.rewrittenUrl; - this.url = request.url; + // @ts-expect-error request._core isn't supposed to be accessed - remove once we upgrade to hapi v18 + this.url = new URL(request.url.href!, request._core.info.uri); this.headers = deepFreeze({ ...request.headers }); this.isSystemRequest = request.headers['kbn-system-request'] === 'true' || @@ -304,8 +305,8 @@ export class KibanaRequest< if (authOptions === false) return false; throw new Error( `unexpected authentication options: ${JSON.stringify(authOptions)} for route: ${ - this.url.href - }` + this.url.pathname + }${this.url.search}` ); } } diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index e47d06409894e..7551f53ab27de 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -19,6 +19,7 @@ import { of } from 'rxjs'; import { duration } from 'moment'; import { ByteSizeValue } from '@kbn/config-schema'; +import type { MockedKeys } from '@kbn/utility-types/jest'; import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.'; import { loggingSystemMock } from './logging/logging_system.mock'; import { loggingServiceMock } from './logging/logging_service.mock'; diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts index 1d5ce5625bf48..862d11cfa663a 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts @@ -78,7 +78,7 @@ describe('getObjectReferencesToFetch()', () => { `); }); - test(`doesn't deal with circular dependencies`, () => { + test('does not fail on circular dependencies', () => { const map = new Map(); map.set('index-pattern:1', { id: '1', @@ -527,7 +527,7 @@ describe('injectNestedDependencies', () => { `); }); - test(`doesn't deal with circular dependencies`, async () => { + test('does not fail on circular dependencies', async () => { const savedObjects = [ { id: '2', diff --git a/src/core/server/saved_objects/export/sort_objects.test.ts b/src/core/server/saved_objects/export/sort_objects.test.ts index 7b6698dfaf887..cd116d767b0c3 100644 --- a/src/core/server/saved_objects/export/sort_objects.test.ts +++ b/src/core/server/saved_objects/export/sort_objects.test.ts @@ -46,27 +46,27 @@ describe('sortObjects()', () => { }, ]; expect(sortObjects(docs)).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "search", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); }); test('should not mutate parameter', () => { @@ -91,49 +91,49 @@ Array [ }, ]; expect(sortObjects(docs)).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "search", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); expect(docs).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + ] + `); }); test('should sort unordered array', () => { @@ -199,71 +199,71 @@ Array [ }, ]; expect(sortObjects(docs)).toMatchInlineSnapshot(` -Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "3", - "references": Array [ - Object { - "id": "2", - "name": "ref1", - "type": "search", - }, - ], - "type": "visualization", - }, - Object { - "attributes": Object {}, - "id": "4", - "references": Array [ - Object { - "id": "1", - "name": "ref1", - "type": "index-pattern", - }, - ], - "type": "visualization", - }, - Object { - "attributes": Object {}, - "id": "5", - "references": Array [ - Object { - "id": "3", - "name": "ref1", - "type": "visualization", - }, - Object { - "id": "4", - "name": "ref2", - "type": "visualization", - }, - ], - "type": "dashboard", - }, -] -`); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, + "id": "3", + "references": Array [ + Object { + "id": "2", + "name": "ref1", + "type": "search", + }, + ], + "type": "visualization", + }, + Object { + "attributes": Object {}, + "id": "4", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "index-pattern", + }, + ], + "type": "visualization", + }, + Object { + "attributes": Object {}, + "id": "5", + "references": Array [ + Object { + "id": "3", + "name": "ref1", + "type": "visualization", + }, + Object { + "id": "4", + "name": "ref2", + "type": "visualization", + }, + ], + "type": "dashboard", + }, + ] + `); }); - test('detects circular dependencies', () => { + test('should not fail on circular dependencies', () => { const docs = [ { id: '1', @@ -290,8 +290,149 @@ Array [ ], }, ]; - expect(() => sortObjects(docs)).toThrowErrorMatchingInlineSnapshot( - `"circular reference: [foo:1] ref-> [foo:2] ref-> [foo:1]"` - ); + + expect(sortObjects(docs)).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref1", + "type": "foo", + }, + ], + "type": "foo", + }, + Object { + "attributes": Object {}, + "id": "1", + "references": Array [ + Object { + "id": "2", + "name": "ref1", + "type": "foo", + }, + ], + "type": "foo", + }, + ] + `); + }); + test('should not fail on complex circular dependencies', () => { + const docs = [ + { + id: '1', + type: 'foo', + attributes: {}, + references: [ + { + name: 'ref12', + type: 'foo', + id: '2', + }, + { + name: 'ref13', + type: 'baz', + id: '3', + }, + ], + }, + { + id: '2', + type: 'foo', + attributes: {}, + references: [ + { + name: 'ref13', + type: 'foo', + id: '3', + }, + ], + }, + { + id: '3', + type: 'baz', + attributes: {}, + references: [ + { + name: 'ref13', + type: 'xyz', + id: '4', + }, + ], + }, + { + id: '4', + type: 'xyz', + attributes: {}, + references: [ + { + name: 'ref14', + type: 'foo', + id: '1', + }, + ], + }, + ]; + + expect(sortObjects(docs)).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "3", + "name": "ref13", + "type": "foo", + }, + ], + "type": "foo", + }, + Object { + "attributes": Object {}, + "id": "4", + "references": Array [ + Object { + "id": "1", + "name": "ref14", + "type": "foo", + }, + ], + "type": "xyz", + }, + Object { + "attributes": Object {}, + "id": "3", + "references": Array [ + Object { + "id": "4", + "name": "ref13", + "type": "xyz", + }, + ], + "type": "baz", + }, + Object { + "attributes": Object {}, + "id": "1", + "references": Array [ + Object { + "id": "2", + "name": "ref12", + "type": "foo", + }, + Object { + "id": "3", + "name": "ref13", + "type": "baz", + }, + ], + "type": "foo", + }, + ] + `); }); }); diff --git a/src/core/server/saved_objects/export/sort_objects.ts b/src/core/server/saved_objects/export/sort_objects.ts index 64bab9f43bf14..ec83b687527fc 100644 --- a/src/core/server/saved_objects/export/sort_objects.ts +++ b/src/core/server/saved_objects/export/sort_objects.ts @@ -17,7 +17,6 @@ * under the License. */ -import Boom from 'boom'; import { SavedObject } from '../types'; export function sortObjects(savedObjects: SavedObject[]): SavedObject[] { @@ -30,11 +29,7 @@ export function sortObjects(savedObjects: SavedObject[]): SavedObject[] { function includeObjects(objects: SavedObject[]) { for (const object of objects) { if (path.has(object)) { - throw Boom.badRequest( - `circular reference: ${[...path, object] - .map((obj) => `[${obj.type}:${obj.id}]`) - .join(' ref-> ')}` - ); + continue; } const refdObjects = object.references diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index d9dc46d2cad99..914b5fbdb5196 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -162,7 +162,7 @@ import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; -import { Url } from 'url'; +import { URL } from 'url'; // @public export interface AppCategory { @@ -1007,11 +1007,11 @@ export class KibanaRequest>; // (undocumented) readonly socket: IKibanaSocket; - readonly url: Url; + readonly url: URL; readonly uuid: string; } diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index a3531057767d4..4281559c9aa14 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -13,8 +13,7 @@ "types/**/*", "test_helpers/**/*", "utils/**/*", - "index.ts", - "typings.ts" + "index.ts" ], "references": [ { "path": "../test_utils/" } diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh index 1c679bdb40b59..939226b565f79 100644 --- a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh +++ b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh @@ -7,14 +7,17 @@ set_chmod() { chmod -f 660 ${KBN_PATH_CONF}/kibana.yml || true chmod -f 2750 <%= dataDir %> || true chmod -f 2750 ${KBN_PATH_CONF} || true + chmod -f 2750 <%= logDir %> || true } set_chown() { + chown <%= user %>:<%= group %> <%= logDir %> chown -R <%= user %>:<%= group %> <%= dataDir %> chown -R root:<%= group %> ${KBN_PATH_CONF} } -set_access() { +setup() { + [ ! -d "<%= logDir %>" ] && mkdir "<%= logDir %>" set_chmod set_chown } @@ -35,7 +38,7 @@ case $1 in IS_UPGRADE=true fi - set_access + setup ;; abort-deconfigure|abort-upgrade|abort-remove) ;; @@ -55,7 +58,7 @@ case $1 in IS_UPGRADE=true fi - set_access + setup ;; *) diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index b5169ec3d43b6..e5de760ea11d0 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -109,6 +109,8 @@ export async function runFpm( `pluginsDir=/usr/share/kibana/plugins`, '--template-value', `dataDir=/var/lib/kibana`, + '--template-value', + `logDir=/var/log/kibana`, // config and data directories are copied to /usr/share and /var/lib // below, so exclude them from the main package source located in @@ -132,7 +134,6 @@ export async function runFpm( `${resolveWithTrailingSlash(fromBuild('data'))}=/var/lib/kibana/`, // copy package configurations - `${resolveWithTrailingSlash(__dirname, 'service_templates/sysv/')}=/`, `${resolveWithTrailingSlash(__dirname, 'service_templates/systemd/')}=/`, ]; diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana similarity index 100% rename from src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana rename to src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service index e66e0e7c8dfb5..05724db8799f3 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/etc/systemd/system/kibana.service @@ -1,21 +1,32 @@ [Unit] Description=Kibana +Documentation=https://www.elastic.co +Wants=network-online.target +After=network-online.target [Service] Type=simple User=kibana Group=kibana -# Load env vars from /etc/default/ and /etc/sysconfig/ if they exist. -# Prefixing the path with '-' makes it try to load, but if the file doesn't -# exist, it continues onward. + +Environment=KBN_HOME=/usr/share/kibana +Environment=KBN_PATH_CONF=/etc/kibana + EnvironmentFile=-/etc/default/kibana EnvironmentFile=-/etc/sysconfig/kibana -ExecStart=/usr/share/kibana/bin/kibana + +ExecStart=/usr/share/kibana/bin/kibana --logging.dest="/var/log/kibana/kibana.log" + Restart=on-failure RestartSec=3 + StartLimitBurst=3 StartLimitInterval=60 -WorkingDirectory=/ + +WorkingDirectory=/usr/share/kibana + +StandardOutput=journal +StandardError=inherit [Install] WantedBy=multi-user.target diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana deleted file mode 100755 index c13676ef031b0..0000000000000 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ /dev/null @@ -1,177 +0,0 @@ -#!/bin/sh -# Init script for kibana -# Maintained by -# Generated by pleaserun. -# Implemented based on LSB Core 3.1: -# * Sections: 20.2, 20.3 -# -### BEGIN INIT INFO -# Provides: kibana -# Required-Start: $remote_fs $syslog -# Required-Stop: $remote_fs $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: -# Description: Kibana -### END INIT INFO - -# -# Source function libraries if present. -# (It improves integration with systemd) -# -# Red Hat -if [ -f /etc/rc.d/init.d/functions ]; then - . /etc/rc.d/init.d/functions - -# Debian -elif [ -f /lib/lsb/init-functions ]; then - . /lib/lsb/init-functions - -# SUSE -elif [ -f /etc/rc.status ]; then - . /etc/rc.status - rc_reset -fi - -name=kibana -program=/usr/share/kibana/bin/kibana -pidfile="/var/run/kibana/$name.pid" - -[ -r /etc/default/$name ] && . /etc/default/$name -[ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name - -export KBN_PATH_CONF -export NODE_OPTIONS - -[ -z "$nice" ] && nice=0 - -trace() { - logger -t "/etc/init.d/kibana" "$@" -} - -emit() { - trace "$@" - echo "$@" -} - -start() { - [ ! -d "/var/log/kibana/" ] && mkdir "/var/log/kibana/" - chown "$user":"$group" "/var/log/kibana/" - chmod 2750 "/var/log/kibana/" - - [ ! -d "/var/run/kibana/" ] && mkdir "/var/run/kibana/" - chown "$user":"$group" "/var/run/kibana/" - chmod 755 "/var/run/kibana/" - - chroot --userspec "$user":"$group" "$chroot" sh -c " - - cd \"$chdir\" - exec \"$program\" - " >> /var/log/kibana/kibana.stdout 2>> /var/log/kibana/kibana.stderr & - - # Generate the pidfile from here. If we instead made the forked process - # generate it there will be a race condition between the pidfile writing - # and a process possibly asking for status. - echo $! > $pidfile - - emit "$name started" - return 0 -} - -stop() { - # Try a few times to kill TERM the program - if status ; then - pid=$(cat "$pidfile") - trace "Killing $name (pid $pid) with SIGTERM" - kill -TERM $pid - # Wait for it to exit. - for i in 1 2 3 4 5 ; do - trace "Waiting $name (pid $pid) to die..." - status || break - sleep 1 - done - if status ; then - if [ "$KILL_ON_STOP_TIMEOUT" -eq 1 ] ; then - trace "Timeout reached. Killing $name (pid $pid) with SIGKILL. This may result in data loss." - kill -KILL $pid - emit "$name killed with SIGKILL." - else - emit "$name stop failed; still running." - fi - else - emit "$name stopped." - fi - fi -} - -status() { - if [ -f "$pidfile" ] ; then - pid=$(cat "$pidfile") - if ps -p $pid > /dev/null 2> /dev/null ; then - # process by this pid is running. - # It may not be our pid, but that's what you get with just pidfiles. - # TODO(sissel): Check if this process seems to be the same as the one we - # expect. It'd be nice to use flock here, but flock uses fork, not exec, - # so it makes it quite awkward to use in this case. - return 0 - else - return 2 # program is dead but pid file exists - fi - else - return 3 # program is not running - fi -} - -force_stop() { - if status ; then - stop - status && kill -KILL $(cat "$pidfile") - fi -} - - -case "$1" in - force-start|start|stop|force-stop|restart) - trace "Attempting '$1' on kibana" - ;; -esac - -case "$1" in - force-start) - PRESTART=no - exec "$0" start - ;; - start) - status - code=$? - if [ $code -eq 0 ]; then - emit "$name is already running" - exit $code - else - start - exit $? - fi - ;; - stop) stop ;; - force-stop) force_stop ;; - status) - status - code=$? - if [ $code -eq 0 ] ; then - emit "$name is running" - else - emit "$name is not running" - fi - exit $code - ;; - restart) - - stop && start - ;; - *) - echo "Usage: $SCRIPTNAME {start|force-start|stop|force-start|force-stop|status|restart}" >&2 - exit 3 - ;; -esac - -exit $? diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 8ec80ac295c73..5dac270239c4a 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -24,6 +24,10 @@ export NODE_OPTIONS="$NODE_OPTIONS --max-old-space-size=4096" ### export FORCE_COLOR=1 +### APM tracking +### +export ELASTIC_APM_ENVIRONMENT=ci + ### ### check that we seem to be in a kibana project ### diff --git a/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts b/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts index af9fef0bbd297..791c366199d74 100644 --- a/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts +++ b/src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts @@ -49,7 +49,7 @@ const someSchema: MakeSchemaFrom> = { const someOtherSchema: MakeSchemaFrom> = { my_objects: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean' }, }, diff --git a/src/fixtures/telemetry_collectors/working_collector.ts b/src/fixtures/telemetry_collectors/working_collector.ts index 0a3bf49638a7b..f9cb3bb568673 100644 --- a/src/fixtures/telemetry_collectors/working_collector.ts +++ b/src/fixtures/telemetry_collectors/working_collector.ts @@ -85,7 +85,7 @@ export const myCollector = makeUsageCollector({ }, my_objects: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean' }, }, @@ -93,17 +93,17 @@ export const myCollector = makeUsageCollector({ type: 'array', items: { total: { - type: 'number', + type: 'long', }, type: { type: 'boolean' }, }, }, my_str_array: { type: 'array', items: { type: 'keyword' } }, my_index_signature_prop: { - count: { type: 'number' }, - avg: { type: 'number' }, - max: { type: 'number' }, - min: { type: 'number' }, + count: { type: 'long' }, + avg: { type: 'float' }, + max: { type: 'long' }, + min: { type: 'long' }, }, }, }); diff --git a/src/legacy/server/i18n/get_kibana_translation_paths.test.ts b/src/legacy/server/i18n/get_kibana_translation_paths.test.ts new file mode 100644 index 0000000000000..0f202c4d433c0 --- /dev/null +++ b/src/legacy/server/i18n/get_kibana_translation_paths.test.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { I18N_RC } from './constants'; +import { fromRoot } from '../../../core/server/utils'; + +jest.mock('./get_translation_paths', () => ({ getTranslationPaths: jest.fn() })); +import { getKibanaTranslationPaths } from './get_kibana_translation_paths'; +import { getTranslationPaths as mockGetTranslationPaths } from './get_translation_paths'; + +describe('getKibanaTranslationPaths', () => { + const mockConfig = { get: jest.fn() }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('calls getTranslationPaths against kibana root and kibana-extra', async () => { + mockConfig.get.mockReturnValue([]); + await getKibanaTranslationPaths(mockConfig); + expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(1, { + cwd: fromRoot('.'), + glob: `*/${I18N_RC}`, + }); + + expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(2, { + cwd: fromRoot('../kibana-extra'), + glob: `*/${I18N_RC}`, + }); + }); + + it('calls getTranslationPaths for each config returned in plugin.paths and plugins.scanDirs', async () => { + mockConfig.get.mockReturnValueOnce(['a', 'b']).mockReturnValueOnce(['c']); + await getKibanaTranslationPaths(mockConfig); + expect(mockConfig.get).toHaveBeenNthCalledWith(1, 'plugins.paths'); + expect(mockConfig.get).toHaveBeenNthCalledWith(2, 'plugins.scanDirs'); + + expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(2, { cwd: 'a', glob: I18N_RC }); + expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(3, { cwd: 'b', glob: I18N_RC }); + expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(4, { cwd: 'c', glob: `*/${I18N_RC}` }); + }); +}); diff --git a/src/legacy/server/i18n/get_kibana_translation_paths.ts b/src/legacy/server/i18n/get_kibana_translation_paths.ts new file mode 100644 index 0000000000000..d7f77d3185ba4 --- /dev/null +++ b/src/legacy/server/i18n/get_kibana_translation_paths.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KibanaConfig } from '../kbn_server'; +import { fromRoot } from '../../../core/server/utils'; +import { I18N_RC } from './constants'; +import { getTranslationPaths } from './get_translation_paths'; + +export async function getKibanaTranslationPaths(config: Pick) { + return await Promise.all([ + getTranslationPaths({ + cwd: fromRoot('.'), + glob: `*/${I18N_RC}`, + }), + ...(config.get('plugins.paths') as string[]).map((cwd) => + getTranslationPaths({ cwd, glob: I18N_RC }) + ), + ...(config.get('plugins.scanDirs') as string[]).map((cwd) => + getTranslationPaths({ cwd, glob: `*/${I18N_RC}` }) + ), + getTranslationPaths({ + cwd: fromRoot('../kibana-extra'), + glob: `*/${I18N_RC}`, + }), + ]); +} diff --git a/src/legacy/server/i18n/get_translations_path.ts b/src/legacy/server/i18n/get_translation_paths.ts similarity index 100% rename from src/legacy/server/i18n/get_translations_path.ts rename to src/legacy/server/i18n/get_translation_paths.ts diff --git a/src/legacy/server/i18n/i18n_mixin.ts b/src/legacy/server/i18n/i18n_mixin.ts new file mode 100644 index 0000000000000..4f77fa8df96cd --- /dev/null +++ b/src/legacy/server/i18n/i18n_mixin.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n, i18nLoader } from '@kbn/i18n'; +import { basename } from 'path'; +import { Server } from 'hapi'; +import type { UsageCollectionSetup } from '../../../plugins/usage_collection/server'; +import { getKibanaTranslationPaths } from './get_kibana_translation_paths'; +import KbnServer, { KibanaConfig } from '../kbn_server'; +import { registerLocalizationUsageCollector } from './localization'; + +export async function i18nMixin( + kbnServer: KbnServer, + server: Server, + config: Pick +) { + const locale = config.get('i18n.locale') as string; + + const translationPaths = await getKibanaTranslationPaths(config); + + const currentTranslationPaths = ([] as string[]) + .concat(...translationPaths) + .filter((translationPath) => basename(translationPath, '.json') === locale); + i18nLoader.registerTranslationFiles(currentTranslationPaths); + + const translations = await i18nLoader.getTranslationsByLocale(locale); + i18n.init( + Object.freeze({ + locale, + ...translations, + }) + ); + + const getTranslationsFilePaths = () => currentTranslationPaths; + + server.decorate('server', 'getTranslationsFilePaths', getTranslationsFilePaths); + + if (kbnServer.newPlatform.setup.plugins.usageCollection) { + const { usageCollection } = kbnServer.newPlatform.setup.plugins as { + usageCollection: UsageCollectionSetup; + }; + registerLocalizationUsageCollector(usageCollection, { + getLocale: () => config.get('i18n.locale') as string, + getTranslationsFilePaths, + }); + } +} diff --git a/src/legacy/server/i18n/index.ts b/src/legacy/server/i18n/index.ts index 61caefb2fb599..a7ef49f44532c 100644 --- a/src/legacy/server/i18n/index.ts +++ b/src/legacy/server/i18n/index.ts @@ -17,60 +17,4 @@ * under the License. */ -import { i18n, i18nLoader } from '@kbn/i18n'; -import { basename } from 'path'; -import { Server } from 'hapi'; -import { fromRoot } from '../../../core/server/utils'; -import type { UsageCollectionSetup } from '../../../plugins/usage_collection/server'; -import { getTranslationPaths } from './get_translations_path'; -import { I18N_RC } from './constants'; -import KbnServer, { KibanaConfig } from '../kbn_server'; -import { registerLocalizationUsageCollector } from './localization'; - -export async function i18nMixin(kbnServer: KbnServer, server: Server, config: KibanaConfig) { - const locale = config.get('i18n.locale') as string; - - const translationPaths = await Promise.all([ - getTranslationPaths({ - cwd: fromRoot('.'), - glob: `*/${I18N_RC}`, - }), - ...(config.get('plugins.paths') as string[]).map((cwd) => - getTranslationPaths({ cwd, glob: I18N_RC }) - ), - ...(config.get('plugins.scanDirs') as string[]).map((cwd) => - getTranslationPaths({ cwd, glob: `*/${I18N_RC}` }) - ), - getTranslationPaths({ - cwd: fromRoot('../kibana-extra'), - glob: `*/${I18N_RC}`, - }), - ]); - - const currentTranslationPaths = ([] as string[]) - .concat(...translationPaths) - .filter((translationPath) => basename(translationPath, '.json') === locale); - i18nLoader.registerTranslationFiles(currentTranslationPaths); - - const translations = await i18nLoader.getTranslationsByLocale(locale); - i18n.init( - Object.freeze({ - locale, - ...translations, - }) - ); - - const getTranslationsFilePaths = () => currentTranslationPaths; - - server.decorate('server', 'getTranslationsFilePaths', getTranslationsFilePaths); - - if (kbnServer.newPlatform.setup.plugins.usageCollection) { - const { usageCollection } = kbnServer.newPlatform.setup.plugins as { - usageCollection: UsageCollectionSetup; - }; - registerLocalizationUsageCollector(usageCollection, { - getLocale: () => config.get('i18n.locale') as string, - getTranslationsFilePaths, - }); - } -} +export { i18nMixin } from './i18n_mixin'; diff --git a/src/plugins/console/README.md b/src/plugins/console/README.md new file mode 100644 index 0000000000000..07421151f8087 --- /dev/null +++ b/src/plugins/console/README.md @@ -0,0 +1,5 @@ +# Console + +## About + +Console provides the user with tools for storing and executing requests against Elasticsearch. \ No newline at end of file diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx b/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx index 8bc81b3296c3d..6ec5b0d637556 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx @@ -72,7 +72,7 @@ export function LibraryNotificationPopover({

{i18n.translate('dashboard.panel.libraryNotification.toolTip', { defaultMessage: - 'This panel is linked to a library item. Editing the panel might affect other dashboards.', + 'Editing this panel might affect other dashboards. To change to this panel only, unlink it from the library.', })}

diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index fa45e433050ab..e5947b73b305b 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -139,7 +139,7 @@ export class DashboardAppController { dashboardCapabilities, scopedHistory, embeddableCapabilities: { visualizeCapabilities, mapsCapabilities }, - data: { query: queryService }, + data: { query: queryService, search: searchService }, core: { notifications, overlays, @@ -412,8 +412,9 @@ export class DashboardAppController { >(DASHBOARD_CONTAINER_TYPE); if (dashboardFactory) { + const searchSessionId = searchService.session.start(); dashboardFactory - .create(getDashboardInput()) + .create({ ...getDashboardInput(), searchSessionId }) .then((container: DashboardContainer | ErrorEmbeddable | undefined) => { if (container && !isErrorEmbeddable(container)) { dashboardContainer = container; @@ -572,7 +573,7 @@ export class DashboardAppController { differences.filters = appStateDashboardInput.filters; } - Object.keys(_.omit(containerInput, ['filters'])).forEach((key) => { + Object.keys(_.omit(containerInput, ['filters', 'searchSessionId'])).forEach((key) => { const containerValue = (containerInput as { [key: string]: unknown })[key]; const appStateValue = ((appStateDashboardInput as unknown) as { [key: string]: unknown })[ key @@ -590,7 +591,8 @@ export class DashboardAppController { const refreshDashboardContainer = () => { const changes = getChangesFromAppStateForContainerState(); if (changes && dashboardContainer) { - dashboardContainer.updateInput(changes); + const searchSessionId = searchService.session.start(); + dashboardContainer.updateInput({ ...changes, searchSessionId }); } }; @@ -1109,12 +1111,6 @@ export class DashboardAppController { $scope.model.filters = filterManager.getFilters(); $scope.model.query = queryStringManager.getQuery(); dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); - if (dashboardContainer) { - dashboardContainer.updateInput({ - filters: $scope.model.filters, - query: $scope.model.query, - }); - } }, }); @@ -1159,6 +1155,7 @@ export class DashboardAppController { if (dashboardContainer) { dashboardContainer.destroy(); } + searchService.session.clear(); }); } } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx index a7226082d3dce..89aacf2a84029 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx @@ -134,3 +134,25 @@ test('Container view mode change propagates to new children', async () => { expect(embeddable.getInput().viewMode).toBe(ViewMode.EDIT); }); + +test('searchSessionId propagates to children', async () => { + const searchSessionId1 = 'searchSessionId1'; + const container = new DashboardContainer( + getSampleDashboardInput({ searchSessionId: searchSessionId1 }), + options + ); + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Bob', + }); + + expect(embeddable.getInput().searchSessionId).toBe(searchSessionId1); + + const searchSessionId2 = 'searchSessionId2'; + container.updateInput({ searchSessionId: searchSessionId2 }); + + expect(embeddable.getInput().searchSessionId).toBe(searchSessionId2); +}); diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index 036880a1d088b..757488185fe8e 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -78,6 +78,7 @@ export interface InheritedChildInput extends IndexSignature { viewMode: ViewMode; hidePanelTitles?: boolean; id: string; + searchSessionId?: string; } export interface DashboardContainerOptions { @@ -228,7 +229,15 @@ export class DashboardContainer extends Container; diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index af956a20c0dc5..5a830586b8d05 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { PublicMethodsOf } from '@kbn/utility-types'; import { GetConfigFn } from '../types'; import { FieldFormat } from './field_format'; import { FieldFormatsRegistry } from './field_formats_registry'; 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 fd3d7a1d138fd..aae9b89cdc61f 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 @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsClientCommon } from '../..'; import { createIndexPatternCache } from '.'; diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index bcf2101704c80..160860bcce591 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -44,6 +44,8 @@ describe('Aggs service', () => { }; startDeps = { getConfig: jest.fn(), + getIndexPattern: jest.fn(), + isDefaultTimezone: jest.fn(), }; }); @@ -201,8 +203,9 @@ describe('Aggs service', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(3); + expect(Object.keys(start).length).toBe(4); expect(start).toHaveProperty('calculateAutoTimeExpression'); + expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); }); diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts index 6f3e3904dbbd5..b6afa708f9e6f 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.ts @@ -18,7 +18,7 @@ */ import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; -import { UI_SETTINGS } from '../../../common'; +import { IndexPattern, UI_SETTINGS } from '../../../common'; import { GetConfigFn } from '../../types'; import { AggConfigs, @@ -28,6 +28,7 @@ import { getCalculateAutoTimeExpression, } from './'; import { AggsCommonSetup, AggsCommonStart } from './types'; +import { getDateMetaByDatatableColumn } from './utils/time_column_meta'; /** @internal */ export const aggsRequiredUiSettings = [ @@ -50,6 +51,8 @@ export interface AggsCommonSetupDependencies { /** @internal */ export interface AggsCommonStartDependencies { getConfig: GetConfigFn; + getIndexPattern(id: string): Promise; + isDefaultTimezone: () => boolean; } /** @@ -77,11 +80,22 @@ export class AggsCommonService { }; } - public start({ getConfig }: AggsCommonStartDependencies): AggsCommonStart { + public start({ + getConfig, + getIndexPattern, + isDefaultTimezone, + }: AggsCommonStartDependencies): AggsCommonStart { const aggTypesStart = this.aggTypesRegistry.start(); + const calculateAutoTimeExpression = getCalculateAutoTimeExpression(getConfig); return { - calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), + calculateAutoTimeExpression, + getDateMetaByDatatableColumn: getDateMetaByDatatableColumn({ + calculateAutoTimeExpression, + getIndexPattern, + getConfig, + isDefaultTimezone, + }), createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: aggTypesStart, diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index c273ca53a5fed..694b03f660452 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -34,6 +34,7 @@ import { writeParams } from '../agg_params'; import { isMetricAggType } from '../metrics/metric_agg_type'; import { BaseAggParams } from '../types'; import { dateHistogramInterval } from '../utils'; +import { inferTimeZone } from '../utils'; /** @internal */ export type CalculateBoundsFn = (timeRange: TimeRange) => TimeRangeBounds; @@ -235,25 +236,7 @@ export const getDateHistogramBucketAgg = ({ // time_zones being persisted into saved_objects serialize: noop, write(agg, output) { - // If a time_zone has been set explicitly always prefer this. - let tz = agg.params.time_zone; - if (!tz && agg.params.field) { - // If a field has been configured check the index pattern's typeMeta if a date_histogram on that - // field requires a specific time_zone - tz = get(agg.getIndexPattern(), [ - 'typeMeta', - 'aggs', - 'date_histogram', - agg.params.field.name, - 'time_zone', - ]); - } - if (!tz) { - // If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz - const detectedTimezone = moment.tz.guess(); - const tzOffset = moment().format('Z'); - tz = isDefaultTimezone() ? detectedTimezone || tzOffset : getConfig('dateFormat:tz'); - } + const tz = inferTimeZone(agg.params, agg.getIndexPattern(), isDefaultTimezone, getConfig); output.params.time_zone = tz; }, }, diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index aec3dcc9d068c..09a13762d4d70 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -18,7 +18,9 @@ */ import { Assign } from '@kbn/utility-types'; +import { DatatableColumn } from 'src/plugins/expressions'; import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; +import { TimeRange } from '../../query'; import { AggConfigSerialized, AggConfigs, @@ -80,6 +82,19 @@ export interface AggsCommonSetup { /** @internal */ export interface AggsCommonStart { calculateAutoTimeExpression: ReturnType; + /** + * Helper function returning meta data about use date intervals for a data table column. + * If the column is not a column created by a date histogram aggregation of the esaggs data source, + * this function will return undefined. + * + * Otherwise, it will return the following attributes in an object: + * * `timeZone` time zone used to create the buckets (important e.g. for DST), + * * `timeRange` total time range of the fetch data (to infer partial buckets at the beginning and end of the data) + * * `interval` Interval used on elasticsearch (`auto` resolved to the actual interval) + */ + getDateMetaByDatatableColumn: ( + column: DatatableColumn + ) => Promise; createAggConfigs: ( indexPattern: IndexPattern, configStates?: CreateAggConfigParams[], diff --git a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts index 28646c092c01c..826e402b14682 100644 --- a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts @@ -79,6 +79,33 @@ describe('getFormatWithAggs', () => { expect(getFormat).toHaveBeenCalledTimes(1); }); + test('creates alternative format for range using the template parameter', () => { + const mapping = { id: 'range', params: { template: 'arrow_right' } }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert({ gte: 1, lt: 20 })).toBe('1 → 20'); + expect(getFormat).toHaveBeenCalledTimes(1); + }); + + test('handles Infinity values internally when no nestedFormatter is passed', () => { + const mapping = { id: 'range', params: { replaceInfinity: true } }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert({ gte: -Infinity, lt: Infinity })).toBe('≥ −∞ and < +∞'); + expect(getFormat).toHaveBeenCalledTimes(1); + }); + + test('lets Infinity values handling to nestedFormatter even when flag is on', () => { + const mapping = { id: 'range', params: { replaceInfinity: true, id: 'any' } }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert({ gte: -Infinity, lt: Infinity })).toBe('≥ -Infinity and < Infinity'); + expect(getFormat).toHaveBeenCalledTimes(1); + }); + test('returns custom label for range if provided', () => { const mapping = { id: 'range', params: {} }; const getFieldFormat = getFormatWithAggs(getFormat); diff --git a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts index a8134619fec0d..6b03dc5f70edc 100644 --- a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts @@ -56,15 +56,35 @@ export function getFormatWithAggs(getFieldFormat: GetFieldFormat): GetFieldForma id: nestedFormatter.id, params: nestedFormatter.params, }); + const gte = '\u2265'; const lt = '\u003c'; + let fromValue = format.convert(range.gte); + let toValue = format.convert(range.lt); + // In case of identity formatter and a specific flag, replace Infinity values by specific strings + if (params.replaceInfinity && nestedFormatter.id == null) { + const FROM_PLACEHOLDER = '\u2212\u221E'; + const TO_PLACEHOLDER = '+\u221E'; + fromValue = isFinite(range.gte) ? fromValue : FROM_PLACEHOLDER; + toValue = isFinite(range.lt) ? toValue : TO_PLACEHOLDER; + } + + if (params.template === 'arrow_right') { + return i18n.translate('data.aggTypes.buckets.ranges.rangesFormatMessageArrowRight', { + defaultMessage: '{from} → {to}', + values: { + from: fromValue, + to: toValue, + }, + }); + } return i18n.translate('data.aggTypes.buckets.ranges.rangesFormatMessage', { defaultMessage: '{gte} {from} and {lt} {to}', values: { gte, - from: format.convert(range.gte), + from: fromValue, lt, - to: format.convert(range.lt), + to: toValue, }, }); }); diff --git a/src/plugins/data/common/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts index 99ce44207d80d..7d6cb1c7ef33f 100644 --- a/src/plugins/data/common/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -23,3 +23,4 @@ export * from './get_format_with_aggs'; export * from './ipv4_address'; export * from './prop_filter'; export * from './to_angular_json'; +export * from './infer_time_zone'; diff --git a/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts b/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts new file mode 100644 index 0000000000000..8fc3726ee1b32 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/infer_time_zone.test.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('moment', () => { + const moment: any = jest.fn(() => { + return { + format: jest.fn(() => '-1;00'), + }; + }); + moment.tz = { + guess: jest.fn(() => 'CET'), + }; + return moment; +}); + +import { IndexPattern } from '../../../index_patterns'; +import { AggParamsDateHistogram } from '../buckets'; +import { inferTimeZone } from './infer_time_zone'; + +describe('inferTimeZone', () => { + it('reads time zone from agg params', () => { + const params: AggParamsDateHistogram = { + time_zone: 'CEST', + }; + expect(inferTimeZone(params, {} as IndexPattern, () => false, jest.fn())).toEqual('CEST'); + }); + + it('reads time zone from index pattern type meta if available', () => { + expect( + inferTimeZone( + { field: 'mydatefield' }, + ({ + typeMeta: { + aggs: { + date_histogram: { + mydatefield: { + time_zone: 'UTC', + }, + }, + }, + }, + } as unknown) as IndexPattern, + () => false, + jest.fn() + ) + ).toEqual('UTC'); + }); + + it('reads time zone from moment if set to default', () => { + expect(inferTimeZone({}, {} as IndexPattern, () => true, jest.fn())).toEqual('CET'); + }); + + it('reads time zone from config if not set to default', () => { + expect( + inferTimeZone( + {}, + {} as IndexPattern, + () => false, + () => 'CET' as any + ) + ).toEqual('CET'); + }); +}); diff --git a/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts b/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts new file mode 100644 index 0000000000000..282238c5a0459 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/infer_time_zone.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; +import { IndexPattern } from '../../../index_patterns'; +import { AggParamsDateHistogram } from '../buckets'; + +export function inferTimeZone( + params: AggParamsDateHistogram, + indexPattern: IndexPattern, + isDefaultTimezone: () => boolean, + getConfig: (key: string) => T +) { + let tz = params.time_zone; + if (!tz && params.field) { + // If a field has been configured check the index pattern's typeMeta if a date_histogram on that + // field requires a specific time_zone + tz = indexPattern.typeMeta?.aggs?.date_histogram?.[params.field]?.time_zone; + } + if (!tz) { + // If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz + const detectedTimezone = moment.tz.guess(); + const tzOffset = moment().format('Z'); + tz = isDefaultTimezone() + ? detectedTimezone || tzOffset + : // if timezone is not the default, this will always return a string + (getConfig('dateFormat:tz') as string); + } + return tz; +} diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts new file mode 100644 index 0000000000000..e56d622734554 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BUCKET_TYPES } from '../buckets'; +import { DateMetaByColumnDeps, getDateMetaByDatatableColumn } from './time_column_meta'; + +describe('getDateMetaByDatatableColumn', () => { + let params: DateMetaByColumnDeps; + beforeEach(() => { + params = { + calculateAutoTimeExpression: jest.fn().mockReturnValue('5m'), + getIndexPattern: jest.fn().mockResolvedValue({}), + isDefaultTimezone: jest.fn().mockReturnValue(true), + getConfig: jest.fn(), + }; + }); + + it('returns nothing on column from other data source', async () => { + expect( + await getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'essql', + }, + }) + ).toEqual(undefined); + }); + + it('returns nothing on non date histogram column', async () => { + expect( + await getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.TERMS, + }, + }, + }) + ).toEqual(undefined); + }); + + it('returns time range, time zone and interval', async () => { + expect( + await getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.DATE_HISTOGRAM, + params: { + time_zone: 'UTC', + interval: '1h', + }, + appliedTimeRange: { + from: 'now-5d', + to: 'now', + }, + }, + }, + }) + ).toEqual({ + timeZone: 'UTC', + timeRange: { + from: 'now-5d', + to: 'now', + }, + interval: '1h', + }); + }); + + it('returns resolved auto interval', async () => { + expect( + await getDateMetaByDatatableColumn(params)({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.DATE_HISTOGRAM, + params: { + time_zone: 'UTC', + interval: 'auto', + }, + appliedTimeRange: { + from: 'now-5d', + to: 'now', + }, + }, + }, + }) + ).toEqual( + expect.objectContaining({ + interval: '5m', + }) + ); + }); +}); diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.ts new file mode 100644 index 0000000000000..1bea716c6a049 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/time_column_meta.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DatatableColumn } from 'src/plugins/expressions/common'; +import { IndexPattern } from '../../../index_patterns'; + +import { TimeRange } from '../../../types'; +import { AggParamsDateHistogram, BUCKET_TYPES } from '../buckets'; +import { inferTimeZone } from './infer_time_zone'; + +export interface DateMetaByColumnDeps { + calculateAutoTimeExpression: (range: TimeRange) => string | undefined; + getIndexPattern: (id: string) => Promise; + isDefaultTimezone: () => boolean; + getConfig: (key: string) => T; +} + +export const getDateMetaByDatatableColumn = ({ + calculateAutoTimeExpression, + getIndexPattern, + isDefaultTimezone, + getConfig, +}: DateMetaByColumnDeps) => async ( + column: DatatableColumn +): Promise => { + if (column.meta.source !== 'esaggs') return; + if (column.meta.sourceParams?.type !== BUCKET_TYPES.DATE_HISTOGRAM) return; + const params = column.meta.sourceParams.params as AggParamsDateHistogram; + const appliedTimeRange = column.meta.sourceParams.appliedTimeRange as TimeRange; + + const tz = inferTimeZone( + params, + await getIndexPattern(column.meta.sourceParams.indexPatternId as string), + isDefaultTimezone, + getConfig + ); + + const interval = + params.interval === 'auto' ? calculateAutoTimeExpression(appliedTimeRange) : params.interval; + + if (!interval) { + throw new Error('time interval could not be determined'); + } + + return { + timeZone: tz, + timeRange: appliedTimeRange, + interval, + }; +}; diff --git a/src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts b/src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts index 3badd456bd72a..b13f6ad266546 100644 --- a/src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts +++ b/src/plugins/data/common/search/search_source/legacy/default_search_strategy.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { defaultSearchStrategy } from './default_search_strategy'; import { LegacyFetchHandlers, SearchStrategySearchParams } from './types'; import { BehaviorSubject } from 'rxjs'; diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts index d4c0707f950bb..ea7d6b4441ccf 100644 --- a/src/plugins/data/common/search/search_source/mocks.ts +++ b/src/plugins/data/common/search/search_source/mocks.ts @@ -18,6 +18,7 @@ */ import { BehaviorSubject } from 'rxjs'; +import type { MockedKeys } from '@kbn/utility-types/jest'; import { uiSettingsServiceMock } from '../../../../../core/public/mocks'; import { SearchSource } from './search_source'; diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/common/search/session/mocks.ts index 7d5cd75b57534..2b64bbbd27565 100644 --- a/src/plugins/data/common/search/session/mocks.ts +++ b/src/plugins/data/common/search/session/mocks.ts @@ -23,6 +23,7 @@ export function getSessionServiceMock(): jest.Mocked { return { clear: jest.fn(), start: jest.fn(), + restore: jest.fn(), getSessionId: jest.fn(), getSession$: jest.fn(), }; diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts index 80ab74f1aa14d..6660b8395547f 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/common/search/session/types.ts @@ -34,6 +34,12 @@ export interface ISessionService { * Starts a new session */ start: () => string; + + /** + * Restores existing session + */ + restore: (sessionId: string) => void; + /** * Clears the active session. */ diff --git a/src/plugins/data/public/field_formats/mocks.ts b/src/plugins/data/public/field_formats/mocks.ts index ec1233a085bce..ba1e9de71bab1 100644 --- a/src/plugins/data/public/field_formats/mocks.ts +++ b/src/plugins/data/public/field_formats/mocks.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { FieldFormatsStart, FieldFormatsSetup, FieldFormatsService } from '.'; import { fieldFormatsMock } from '../../common/field_formats/mocks'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index c041511745be2..c54cb36142cbd 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -380,7 +380,7 @@ export { PainlessError, } from './search'; -export type { SearchSource } from './search'; +export type { SearchSource, ISessionService } from './search'; export { ISearchOptions, isErrorResponse, isCompleteResponse, isPartialResponse } from '../common'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 81fa6d4ba20db..b072407a5fe10 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -16,6 +16,7 @@ import { CoreSetup } from 'src/core/public'; import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; +import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; @@ -1414,8 +1415,6 @@ export interface ISearchSetup { // // (undocumented) aggs: AggsSetup; - // Warning: (ae-forgotten-export) The symbol "ISessionService" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ISessionService" session: ISessionService; // Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts // @@ -1431,7 +1430,6 @@ export interface ISearchStart { aggs: AggsStart; search: ISearchGeneric; searchSource: ISearchStartSearchSource; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ISessionService" session: ISessionService; // (undocumented) showError: (e: Error) => void; @@ -1448,6 +1446,17 @@ export interface ISearchStartSearchSource { // @public (undocumented) export const isErrorResponse: (response?: IKibanaSearchResponse | undefined) => boolean | undefined; +// Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ISessionService { + clear: () => void; + getSession$: () => Observable; + getSessionId: () => string | undefined; + restore: (sessionId: string) => void; + start: () => string; +} + // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2285,7 +2294,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/search/aggs/types.ts:113:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/mocks.ts b/src/plugins/data/public/query/mocks.ts index 53c177de0fa39..f8376d7834873 100644 --- a/src/plugins/data/public/query/mocks.ts +++ b/src/plugins/data/public/query/mocks.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Observable } from 'rxjs'; import { QueryService, QuerySetup, QueryStart } from '.'; import { timefilterServiceMock } from './timefilter/timefilter_service.mock'; diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index 50732c99a62d9..90a622fada95b 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -19,6 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import { skip } from 'rxjs/operators'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { CoreStart } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { Query, UI_SETTINGS } from '../../../common'; diff --git a/src/plugins/data/public/query/timefilter/time_history.ts b/src/plugins/data/public/query/timefilter/time_history.ts index fe73fd85b164d..24786c5eed623 100644 --- a/src/plugins/data/public/query/timefilter/time_history.ts +++ b/src/plugins/data/public/query/timefilter/time_history.ts @@ -18,6 +18,7 @@ */ import moment from 'moment'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { PersistedLog } from '../persisted_log'; import { TimeRange } from '../../../common'; diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 01b82087cf354..49a8c68f6916f 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -20,6 +20,7 @@ import _ from 'lodash'; import { Subject, BehaviorSubject } from 'rxjs'; import moment from 'moment'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals'; import { getForceNow } from './lib/get_force_now'; import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types'; diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index 060257a880528..9f1c64a1739a5 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { TimefilterService, TimeHistoryContract, TimefilterContract } from '.'; import { Observable } from 'rxjs'; diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index db25dfb300d11..de747d234b441 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -23,6 +23,7 @@ import { coreMock } from '../../../../../core/public/mocks'; import { expressionsPluginMock } from '../../../../../plugins/expressions/public/mocks'; import { BucketAggType, getAggTypes, MetricAggType } from '../../../common'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; +import { dataPluginMock } from '../../mocks'; import { AggsService, @@ -46,6 +47,7 @@ describe('AggsService - public', () => { }; startDeps = { fieldFormats: fieldFormatsServiceMock.createStartContract(), + indexPatterns: dataPluginMock.createStartContract().indexPatterns, uiSettings, }; }); @@ -86,8 +88,9 @@ describe('AggsService - public', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(3); + expect(Object.keys(start).length).toBe(4); expect(start).toHaveProperty('calculateAutoTimeExpression'); + expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); }); diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 4b088ddfe314f..85e0f604bb8b5 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -32,6 +32,7 @@ import { AggTypesDependencies, } from '../../../common/search/aggs'; import { AggsSetup, AggsStart } from './types'; +import { IndexPatternsContract } from '../../index_patterns'; /** * Aggs needs synchronous access to specific uiSettings. Since settings can change @@ -68,6 +69,7 @@ export interface AggsSetupDependencies { export interface AggsStartDependencies { fieldFormats: FieldFormatsStart; uiSettings: IUiSettingsClient; + indexPatterns: IndexPatternsContract; } /** @@ -94,9 +96,17 @@ export class AggsService { return this.aggsCommonService.setup({ registerFunction }); } - public start({ fieldFormats, uiSettings }: AggsStartDependencies): AggsStart { - const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ + public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { + const isDefaultTimezone = () => uiSettings.isDefault('dateFormat:tz'); + + const { + calculateAutoTimeExpression, + getDateMetaByDatatableColumn, + types, + } = this.aggsCommonService.start({ getConfig: this.getConfig!, + getIndexPattern: indexPatterns.get, + isDefaultTimezone, }); const aggTypesDependencies: AggTypesDependencies = { @@ -106,7 +116,7 @@ export class AggsService { deserialize: fieldFormats.deserialize, getDefaultInstance: fieldFormats.getDefaultInstance, }), - isDefaultTimezone: () => uiSettings.isDefault('dateFormat:tz'), + isDefaultTimezone, }; // initialize each agg type and store in memory @@ -137,6 +147,7 @@ export class AggsService { return { calculateAutoTimeExpression, + getDateMetaByDatatableColumn, createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); }, diff --git a/src/plugins/data/public/search/aggs/mocks.ts b/src/plugins/data/public/search/aggs/mocks.ts index ca13343777e63..abc930f00b594 100644 --- a/src/plugins/data/public/search/aggs/mocks.ts +++ b/src/plugins/data/public/search/aggs/mocks.ts @@ -67,6 +67,7 @@ export const searchAggsSetupMock = (): AggsSetup => ({ export const searchAggsStartMock = (): AggsStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), + getDateMetaByDatatableColumn: jest.fn(), createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: mockAggTypesRegistry(), diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts index 9cadb1e796ad6..b87ac11e810c9 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { CoreSetup, CoreStart } from '../../../../../core/public'; import { coreMock } from '../../../../../core/public/mocks'; import { usageCollectionPluginMock, Setup } from '../../../../usage_collection/public/mocks'; diff --git a/src/plugins/data/public/search/errors/es_error.test.tsx b/src/plugins/data/public/search/errors/es_error.test.tsx new file mode 100644 index 0000000000000..db719eb6a70c9 --- /dev/null +++ b/src/plugins/data/public/search/errors/es_error.test.tsx @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EsError } from './es_error'; +import { IEsError } from './types'; + +describe('EsError', () => { + it('contains the same body as the wrapped error', () => { + const error = { + body: { + attributes: { + error: { + type: 'top_level_exception_type', + reason: 'top-level reason', + }, + }, + }, + } as IEsError; + const esError = new EsError(error); + + expect(typeof esError.body).toEqual('object'); + expect(esError.body).toEqual(error.body); + }); +}); diff --git a/src/plugins/data/public/search/errors/es_error.tsx b/src/plugins/data/public/search/errors/es_error.tsx index 53d00159b836b..f1e7eaa707a06 100644 --- a/src/plugins/data/public/search/errors/es_error.tsx +++ b/src/plugins/data/public/search/errors/es_error.tsx @@ -22,22 +22,27 @@ import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { ApplicationStart } from 'kibana/public'; import { KbnError } from '../../../../kibana_utils/common'; import { IEsError } from './types'; -import { getRootCause } from './utils'; +import { getRootCause, getTopLevelCause } from './utils'; export class EsError extends KbnError { + readonly body: IEsError['body']; + constructor(protected readonly err: IEsError) { super('EsError'); + this.body = err.body; } public getErrorMessage(application: ApplicationStart) { const rootCause = getRootCause(this.err)?.reason; + const topLevelCause = getTopLevelCause(this.err)?.reason; + const cause = rootCause ?? topLevelCause; return ( <> - {rootCause ? ( + {cause ? ( - {rootCause} + {cause} ) : null} diff --git a/src/plugins/data/public/search/errors/utils.ts b/src/plugins/data/public/search/errors/utils.ts index d07d9b05e91e9..45a318904665f 100644 --- a/src/plugins/data/public/search/errors/utils.ts +++ b/src/plugins/data/public/search/errors/utils.ts @@ -26,6 +26,10 @@ export function getFailedShards(err: IEsError) { return failedShards ? failedShards[0] : undefined; } +export function getTopLevelCause(err: IEsError) { + return err.body?.attributes?.error; +} + export function getRootCause(err: IEsError) { return getFailedShards(err)?.reason; } diff --git a/src/plugins/data/public/search/es_search/get_es_preference.test.ts b/src/plugins/data/public/search/es_search/get_es_preference.test.ts index 05a74b3e6205a..8b3d335b38d17 100644 --- a/src/plugins/data/public/search/es_search/get_es_preference.test.ts +++ b/src/plugins/data/public/search/es_search/get_es_preference.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { getEsPreference } from './get_es_preference'; import { CoreStart } from '../../../../../core/public'; import { coreMock } from '../../../../../core/public/mocks'; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index de7a4ffce8bd4..dba77d398c8b6 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -65,6 +65,7 @@ export interface RequestHandlerParams { metricsAtAllLevels?: boolean; visParams?: any; abortSignal?: AbortSignal; + searchSessionId?: string; } const name = 'esaggs'; @@ -82,6 +83,7 @@ const handleCourierRequest = async ({ inspectorAdapters, filterManager, abortSignal, + searchSessionId, }: RequestHandlerParams) => { // Create a new search source that inherits the original search source // but has the appropriate timeRange applied via a filter. @@ -143,6 +145,7 @@ const handleCourierRequest = async ({ defaultMessage: 'This request queries Elasticsearch to fetch the data for the visualization.', }), + searchSessionId, } ); request.stats(getRequestInspectorStats(requestSearchSource)); @@ -150,6 +153,7 @@ const handleCourierRequest = async ({ try { const response = await requestSearchSource.fetch({ abortSignal, + sessionId: searchSessionId, }); request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); @@ -248,7 +252,7 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ multi: true, }, }, - async fn(input, args, { inspectorAdapters, abortSignal }) { + async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) { const indexPatterns = getIndexPatterns(); const { filterManager } = getQueryService(); const searchService = getSearchService(); @@ -276,6 +280,7 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ inspectorAdapters: inspectorAdapters as Adapters, filterManager, abortSignal: (abortSignal as unknown) as AbortSignal, + searchSessionId: getSearchSessionId(), }); const table: Datatable = { @@ -293,6 +298,13 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ source: 'esaggs', sourceParams: { indexPatternId: indexPattern.id, + appliedTimeRange: + column.aggConfig.params.field?.name && + input?.timeRange && + args.timeFields && + args.timeFields.includes(column.aggConfig.params.field?.name) + ? { from: input.timeRange.from, to: input.timeRange.to } + : undefined, ...column.aggConfig.serialize(), }, }, diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 86804a819cb0e..1abf3192a4846 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -41,6 +41,7 @@ export { SearchSourceDependencies, SearchSourceFields, SortDirection, + ISessionService, } from '../../common/search'; export { getEsPreference } from './es_search'; diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index e8a728bb9cec3..472caa5e4f45f 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { CoreSetup, CoreStart } from '../../../../core/public'; import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 1afcf4615ab5a..087ca9e4f5c47 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -20,6 +20,7 @@ import { get, memoize, trimEnd } from 'lodash'; import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index b59fa6fa16bf6..20041a02067d9 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { coreMock } from '../../../../core/public/mocks'; import { CoreSetup, CoreStart } from '../../../../core/public'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index f955dc5b6ebd5..3dbabfc68fdbc 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -143,7 +143,7 @@ export class SearchService implements Plugin { }; return { - aggs: this.aggsService.start({ fieldFormats, uiSettings }), + aggs: this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }), search, showError: (e: Error) => { this.searchInterceptor.showError(e); diff --git a/src/plugins/data/public/search/session_service.test.ts b/src/plugins/data/public/search/session_service.test.ts index dd64d187f47d6..bcfd06944d983 100644 --- a/src/plugins/data/public/search/session_service.test.ts +++ b/src/plugins/data/public/search/session_service.test.ts @@ -20,6 +20,7 @@ import { SessionService } from './session_service'; import { ISessionService } from '../../common'; import { coreMock } from '../../../../core/public/mocks'; +import { take, toArray } from 'rxjs/operators'; describe('Session service', () => { let sessionService: ISessionService; @@ -39,5 +40,20 @@ describe('Session service', () => { sessionService.clear(); expect(sessionService.getSessionId()).toBeUndefined(); }); + + it('Restores a session', async () => { + const sessionId = 'sessionId'; + sessionService.restore(sessionId); + expect(sessionService.getSessionId()).toBe(sessionId); + }); + + it('sessionId$ observable emits current value', async () => { + sessionService.restore('1'); + const emittedValues = sessionService.getSession$().pipe(take(3), toArray()).toPromise(); + sessionService.restore('2'); + sessionService.clear(); + + expect(await emittedValues).toEqual(['1', '2', undefined]); + }); }); }); diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts index 31524434af302..a172738812937 100644 --- a/src/plugins/data/public/search/session_service.ts +++ b/src/plugins/data/public/search/session_service.ts @@ -18,14 +18,16 @@ */ import uuid from 'uuid'; -import { Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subscription } from 'rxjs'; import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; -import { ISessionService } from '../../common/search'; import { ConfigSchema } from '../../config'; +import { ISessionService } from '../../common/search'; export class SessionService implements ISessionService { - private sessionId?: string; - private session$: Subject = new Subject(); + private session$ = new BehaviorSubject(undefined); + private get sessionId() { + return this.session$.getValue(); + } private appChangeSubscription$?: Subscription; private curApp?: string; @@ -68,13 +70,15 @@ export class SessionService implements ISessionService { } public start() { - this.sessionId = uuid.v4(); - this.session$.next(this.sessionId); - return this.sessionId; + this.session$.next(uuid.v4()); + return this.sessionId!; + } + + public restore(sessionId: string) { + this.session$.next(sessionId); } public clear() { - this.sessionId = undefined; - this.session$.next(this.sessionId); + this.session$.next(undefined); } } diff --git a/src/plugins/data/server/index_patterns/mocks.ts b/src/plugins/data/server/index_patterns/mocks.ts index 8f95afe3b3c9d..52d8aa1e35093 100644 --- a/src/plugins/data/server/index_patterns/mocks.ts +++ b/src/plugins/data/server/index_patterns/mocks.ts @@ -19,6 +19,6 @@ export function createIndexPatternsStartMock() { return { - indexPatternsServiceFactory: jest.fn(), + indexPatternsServiceFactory: jest.fn().mockResolvedValue({ get: jest.fn() }), }; } diff --git a/src/plugins/data/server/search/aggs/aggs_service.test.ts b/src/plugins/data/server/search/aggs/aggs_service.test.ts index d9a945a15fb67..cb4239cc339c4 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.test.ts @@ -23,6 +23,7 @@ import { coreMock } from '../../../../../core/server/mocks'; import { expressionsPluginMock } from '../../../../../plugins/expressions/server/mocks'; import { BucketAggType, getAggTypes, MetricAggType } from '../../../common'; import { createFieldFormatsStartMock } from '../../field_formats/mocks'; +import { createIndexPatternsStartMock } from '../../index_patterns/mocks'; import { AggsService, AggsSetupDependencies, AggsStartDependencies } from './aggs_service'; @@ -40,6 +41,7 @@ describe('AggsService - server', () => { }; startDeps = { fieldFormats: createFieldFormatsStartMock(), + indexPatterns: createIndexPatternsStartMock(), uiSettings, }; }); diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index 3e5cd8adb44a6..c805c8af6694c 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -30,6 +30,7 @@ import { TimeRange, } from '../../../common'; import { FieldFormatsStart } from '../../field_formats'; +import { IndexPatternsServiceStart } from '../../index_patterns'; import { AggsSetup, AggsStart } from './types'; /** @internal */ @@ -41,6 +42,7 @@ export interface AggsSetupDependencies { export interface AggsStartDependencies { fieldFormats: FieldFormatsStart; uiSettings: UiSettingsServiceStart; + indexPatterns: IndexPatternsServiceStart; } /** @@ -61,7 +63,7 @@ export class AggsService { return this.aggsCommonService.setup({ registerFunction }); } - public start({ fieldFormats, uiSettings }: AggsStartDependencies): AggsStart { + public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { return { asScopedToClient: async (savedObjectsClient: SavedObjectsClientContract) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); @@ -72,8 +74,18 @@ export class AggsService { const getConfig = (key: string): T => { return uiSettingsCache[key]; }; + const isDefaultTimezone = () => getConfig('dateFormat:tz') === 'Browser'; - const { calculateAutoTimeExpression, types } = this.aggsCommonService.start({ getConfig }); + const { + calculateAutoTimeExpression, + getDateMetaByDatatableColumn, + types, + } = this.aggsCommonService.start({ + getConfig, + getIndexPattern: (await indexPatterns.indexPatternsServiceFactory(savedObjectsClient)) + .get, + isDefaultTimezone, + }); const aggTypesDependencies: AggTypesDependencies = { calculateBounds: this.calculateBounds, @@ -87,7 +99,7 @@ export class AggsService { * default timezone, but `isDefault` is not currently offered on the * server, so we need to manually check for the default value. */ - isDefaultTimezone: () => getConfig('dateFormat:tz') === 'Browser', + isDefaultTimezone, }; const typesRegistry = { @@ -109,6 +121,7 @@ export class AggsService { return { calculateAutoTimeExpression, + getDateMetaByDatatableColumn, createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); }, diff --git a/src/plugins/data/server/search/aggs/mocks.ts b/src/plugins/data/server/search/aggs/mocks.ts index b50e22fe87b7c..be060de73b9ff 100644 --- a/src/plugins/data/server/search/aggs/mocks.ts +++ b/src/plugins/data/server/search/aggs/mocks.ts @@ -68,6 +68,7 @@ export const searchAggsSetupMock = (): AggsSetup => ({ const commonStartMock = (): AggsCommonStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), + getDateMetaByDatatableColumn: jest.fn(), createAggConfigs: jest.fn().mockImplementation((indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { typesRegistry: mockAggTypesRegistry(), diff --git a/src/plugins/data/server/search/collectors/register.ts b/src/plugins/data/server/search/collectors/register.ts index ab0ea93edd49e..5db4f52169350 100644 --- a/src/plugins/data/server/search/collectors/register.ts +++ b/src/plugins/data/server/search/collectors/register.ts @@ -37,9 +37,9 @@ export async function registerUsageCollector( isReady: () => true, fetch: fetchProvider(context.config.legacy.globalConfig$), schema: { - successCount: { type: 'number' }, - errorCount: { type: 'number' }, - averageDuration: { type: 'long' }, + successCount: { type: 'long' }, + errorCount: { type: 'long' }, + averageDuration: { type: 'float' }, }, }); usageCollection.registerCollector(collector); diff --git a/src/plugins/data/server/search/routes/call_msearch.test.ts b/src/plugins/data/server/search/routes/call_msearch.test.ts index 3d409e22aaa88..183c2334b4e32 100644 --- a/src/plugins/data/server/search/routes/call_msearch.test.ts +++ b/src/plugins/data/server/search/routes/call_msearch.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { Observable } from 'rxjs'; import { IUiSettingsClient, IScopedClusterClient, SharedGlobalConfig } from 'src/core/server'; diff --git a/src/plugins/data/server/search/routes/msearch.test.ts b/src/plugins/data/server/search/routes/msearch.test.ts index 2f414fe0b4920..e2e5818cf9a72 100644 --- a/src/plugins/data/server/search/routes/msearch.test.ts +++ b/src/plugins/data/server/search/routes/msearch.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { Observable } from 'rxjs'; import { diff --git a/src/plugins/data/server/search/routes/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts index 834e5de5c3121..845ab3bbe4eb1 100644 --- a/src/plugins/data/server/search/routes/search.test.ts +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { Observable, from } from 'rxjs'; import { diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index a001d56b36514..2b513be147e9d 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { CoreSetup, CoreStart } from '../../../../core/server'; import { coreMock } from '../../../../core/server/mocks'; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 0130d3aacc91f..04ee0e95c7f08 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -149,7 +149,7 @@ export class SearchService implements Plugin { { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { return { - aggs: this.aggsService.start({ fieldFormats, uiSettings }), + aggs: this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }), getSearchStrategy: this.getSearchStrategy, search: this.search.bind(this), searchSource: { diff --git a/src/plugins/data/server/search/search_source/mocks.ts b/src/plugins/data/server/search/search_source/mocks.ts index 7e9cc8f2ff42c..73b39b416316e 100644 --- a/src/plugins/data/server/search/search_source/mocks.ts +++ b/src/plugins/data/server/search/search_source/mocks.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { KibanaRequest } from 'src/core/server'; import { searchSourceCommonMock } from '../../../common/search/search_source/mocks'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 97cbb40c13db0..a3edbbd3844b3 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -13,6 +13,7 @@ import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CoreStart } from 'src/core/server'; import { CoreStart as CoreStart_2 } from 'kibana/server'; +import { DatatableColumn } from 'src/plugins/expressions'; import { Duration } from 'moment'; import { ElasticsearchClient } from 'kibana/server'; import { Ensure } from '@kbn/utility-types'; @@ -37,6 +38,7 @@ import { PathConfigType } from '@kbn/utils'; import { Plugin as Plugin_2 } from 'src/core/server'; import { Plugin as Plugin_3 } from 'kibana/server'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { RecursiveReadonly } from '@kbn/utility-types'; import { RequestAdapter } from 'src/plugins/inspector/common'; import { RequestHandlerContext } from 'src/core/server'; diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index d609a497c4ba1..ef7bc09206176 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -10,7 +10,6 @@ > - - - - - - - + predecessor-count="contextApp.state.queryParameters.predecessorCount" + predecessor-available="contextApp.state.rows.predecessors.length" + predecessor-status="contextApp.state.loadingStatus.predecessors.status" + on-change-predecessor-count="contextApp.actions.fetchGivenPredecessorRows" + successor-count="contextApp.state.queryParameters.successorCount" + successor-available="contextApp.state.rows.successors.length" + successor-status="contextApp.state.loadingStatus.successors.status" + on-change-successor-count="contextApp.actions.fetchGivenSuccessorRows" + > diff --git a/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap b/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap deleted file mode 100644 index 58305ee23cb21..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/__snapshots__/context_app_legacy.test.tsx.snap +++ /dev/null @@ -1,741 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ContextAppLegacy test renders correctly 1`] = ` - - - - - -
-
- -
- -
-
- - - - - -`; - -exports[`ContextAppLegacy test renders loading indicator 1`] = ` - - - - - -
- -
- -
- - Loading... - -
-
-
-
-
-
-
-
-
-
-`; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx index 16d8cd78004f9..25576a9072944 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx @@ -23,6 +23,7 @@ import { IIndexPattern } from '../../../../../data/common/index_patterns'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; +import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; describe('ContextAppLegacy test', () => { const hit = { @@ -48,28 +49,36 @@ describe('ContextAppLegacy test', () => { columns: ['_source'], filter: () => {}, hits: [hit], - infiniteScroll: true, sorting: ['order_date', 'desc'], minimumVisibleRows: 5, indexPattern, status: 'loaded', + defaultStepSize: 5, + predecessorCount: 10, + successorCount: 10, + predecessorAvailable: 10, + successorAvailable: 10, + onChangePredecessorCount: jest.fn(), + onChangeSuccessorCount: jest.fn(), + predecessorStatus: 'loaded', + successorStatus: 'loaded', }; it('renders correctly', () => { const component = mountWithIntl(); - expect(component).toMatchSnapshot(); expect(component.find(DocTableLegacy).length).toBe(1); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(0); + expect(component.find(ActionBar).length).toBe(2); }); it('renders loading indicator', () => { const props = { ...defaultProps }; props.status = 'loading'; const component = mountWithIntl(); - expect(component).toMatchSnapshot(); expect(component.find('DocTableLegacy').length).toBe(0); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(1); + expect(component.find(ActionBar).length).toBe(2); }); }); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index ee8b2f590f71c..afb4a9a981e21 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -17,15 +17,15 @@ * under the License. */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiPanel, EuiText } from '@elastic/eui'; -import { I18nProvider } from '@kbn/i18n/react'; import { DocTableLegacy, DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; import { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; import { LOADING_STATUS } from './constants'; +import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar'; export interface ContextAppProps { columns: string[]; @@ -35,15 +35,64 @@ export interface ContextAppProps { minimumVisibleRows: number; sorting: string[]; status: string; + defaultStepSize: number; + predecessorCount: number; + successorCount: number; + predecessorAvailable: number; + successorAvailable: number; + onChangePredecessorCount: (count: number) => void; + onChangeSuccessorCount: (count: number) => void; + predecessorStatus: string; + successorStatus: string; +} + +const PREDECESSOR_TYPE = 'predecessors'; +const SUCCESSOR_TYPE = 'successors'; + +function isLoading(status: string) { + return status !== LOADING_STATUS.LOADED && status !== LOADING_STATUS.FAILED; } export function ContextAppLegacy(renderProps: ContextAppProps) { - const { hits, filter, sorting, status } = renderProps; - const props = ({ ...renderProps } as unknown) as DocTableLegacyProps; - props.rows = hits; - props.onFilter = filter; - props.sort = sorting.map((el) => [el]); + const status = renderProps.status; const isLoaded = status === LOADING_STATUS.LOADED; + + const actionBarProps = (type: string) => { + const { + defaultStepSize, + successorCount, + predecessorCount, + predecessorAvailable, + successorAvailable, + predecessorStatus, + successorStatus, + onChangePredecessorCount, + onChangeSuccessorCount, + } = renderProps; + const isPredecessorType = type === PREDECESSOR_TYPE; + return { + defaultStepSize, + docCount: isPredecessorType ? predecessorCount : successorCount, + docCountAvailable: isPredecessorType ? predecessorAvailable : successorAvailable, + onChangeCount: isPredecessorType ? onChangePredecessorCount : onChangeSuccessorCount, + isLoading: isPredecessorType ? isLoading(predecessorStatus) : isLoading(successorStatus), + type, + isDisabled: !isLoaded, + } as ActionBarProps; + }; + + const docTableProps = () => { + const { hits, filter, sorting, columns, indexPattern, minimumVisibleRows } = renderProps; + return { + columns, + indexPattern, + minimumVisibleRows, + rows: hits, + onFilter: filter, + sort: sorting.map((el) => [el]), + } as DocTableLegacyProps; + }; + const loadingFeedback = () => { if (status === LOADING_STATUS.UNINITIALIZED || status === LOADING_STATUS.LOADING) { return ( @@ -59,17 +108,20 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { } return null; }; + return ( + {loadingFeedback()} {isLoaded ? (
- +
) : null} +
); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts index af94c5537da28..4a315be513a0d 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -25,8 +25,16 @@ export function createContextAppLegacy(reactDirective: any) { ['indexPattern', { watchDepth: 'reference' }], ['sorting', { watchDepth: 'reference' }], ['columns', { watchDepth: 'collection' }], - ['infiniteScroll', { watchDepth: 'reference' }], ['minimumVisibleRows', { watchDepth: 'reference' }], ['status', { watchDepth: 'reference' }], + ['defaultStepSize', { watchDepth: 'reference' }], + ['predecessorCount', { watchDepth: 'reference' }], + ['predecessorAvailable', { watchDepth: 'reference' }], + ['predecessorStatus', { watchDepth: 'reference' }], + ['onChangePredecessorCount', { watchDepth: 'reference' }], + ['successorCount', { watchDepth: 'reference' }], + ['successorAvailable', { watchDepth: 'reference' }], + ['successorStatus', { watchDepth: 'reference' }], + ['onChangeSuccessorCount', { watchDepth: 'reference' }], ]); } diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index af88cacfcf992..170078076ec6f 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -266,6 +266,8 @@ export class SearchEmbeddable } private fetch = async () => { + const searchSessionId = this.input.searchSessionId; + if (!this.searchScope) return; const { searchSource } = this.savedSearch; @@ -292,7 +294,11 @@ export class SearchEmbeddable const description = i18n.translate('discover.embeddable.inspectorRequestDescription', { defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', }); - const inspectorRequest = this.inspectorAdaptors.requests.start(title, { description }); + + const inspectorRequest = this.inspectorAdaptors.requests.start(title, { + description, + searchSessionId, + }); inspectorRequest.stats(getRequestInspectorStats(searchSource)); searchSource.getSearchRequestBody().then((body: Record) => { inspectorRequest.json(body); @@ -303,6 +309,7 @@ export class SearchEmbeddable // Make the request const resp = await searchSource.fetch({ abortSignal: this.abortController.signal, + sessionId: searchSessionId, }); this.updateOutput({ loading: false, error: undefined }); diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 68b842c934de8..2737f2678ff32 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -67,4 +67,9 @@ export type EmbeddableInput = { * Visualization filters used to narrow down results. */ filters?: Filter[]; + + /** + * Search session id to group searches + */ + searchSessionId?: string; }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 789353ca4abd7..0d2dcf208f2ef 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -70,6 +70,7 @@ export { isSavedObjectEmbeddableInput, isRangeSelectTriggerContext, isValueClickTriggerContext, + isContextMenuTriggerContext, EmbeddableStateTransfer, EmbeddableEditorState, EmbeddablePackageState, diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.test.tsx index 340d851f3eedf..b020006c0c2bb 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.test.tsx @@ -25,6 +25,7 @@ import { EmbeddableOutput, EmbeddableInput } from './i_embeddable'; import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples/embeddables/contact_card/contact_card_embeddable'; import { FilterableEmbeddable } from '../test_samples/embeddables/filterable_embeddable'; +import type { Filter } from '../../../../data/public'; class TestClass { constructor() {} @@ -79,6 +80,20 @@ test('Embeddable reload is called if lastReloadRequest input time changes', asyn expect(hello.reload).toBeCalledTimes(1); }); +test('Embeddable reload is called if lastReloadRequest input time changed and new input is used', async () => { + const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 0 }); + + const aFilter = ({} as unknown) as Filter; + hello.reload = jest.fn(() => { + // when reload is called embeddable already has new input + expect(hello.getInput().filters).toEqual([aFilter]); + }); + + hello.updateInput({ lastReloadRequestTime: 1, filters: [aFilter] }); + + expect(hello.reload).toBeCalledTimes(1); +}); + test('Embeddable reload is not called if lastReloadRequest input time does not change', async () => { const hello = new FilterableEmbeddable({ id: '123', filters: [], lastReloadRequestTime: 1 }); diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 9267d600360cf..c7afc157c1452 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -195,14 +195,15 @@ export abstract class Embeddable< private onResetInput(newInput: TEmbeddableInput) { if (!isEqual(this.input, newInput)) { - if (this.input.lastReloadRequestTime !== newInput.lastReloadRequestTime) { - this.reload(); - } + const oldLastReloadRequestTime = this.input.lastReloadRequestTime; this.input = newInput; this.input$.next(newInput); this.updateOutput({ title: getPanelTitle(this.input, this.output), } as Partial); + if (oldLastReloadRequestTime !== newInput.lastReloadRequestTime) { + this.reload(); + } } } diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 54c7a2ecc129d..b2965b55dbdfa 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { Datatable } from '../../../../expressions'; import { Trigger } from '../../../../ui_actions/public'; import { IEmbeddable } from '..'; @@ -53,31 +54,49 @@ export type ChartActionContext = | ValueClickContext | RangeSelectContext; -export const isValueClickTriggerContext = ( - context: ChartActionContext -): context is ValueClickContext => context.data && 'data' in context.data; - -export const isRangeSelectTriggerContext = ( - context: ChartActionContext -): context is RangeSelectContext => context.data && 'range' in context.data; - export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { id: CONTEXT_MENU_TRIGGER, - title: 'Context menu', - description: 'Triggered on top-right corner context-menu select.', + title: i18n.translate('embeddableApi.contextMenuTrigger.title', { + defaultMessage: 'Context menu', + }), + description: i18n.translate('embeddableApi.contextMenuTrigger.description', { + defaultMessage: 'A panel top-right corner context menu click.', + }), }; export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = { id: PANEL_BADGE_TRIGGER, - title: 'Panel badges', - description: 'Actions appear in title bar when an embeddable loads in a panel.', + title: i18n.translate('embeddableApi.panelBadgeTrigger.title', { + defaultMessage: 'Panel badges', + }), + description: i18n.translate('embeddableApi.panelBadgeTrigger.description', { + defaultMessage: 'Actions appear in title bar when an embeddable loads in a panel.', + }), }; export const PANEL_NOTIFICATION_TRIGGER = 'PANEL_NOTIFICATION_TRIGGER'; export const panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'> = { id: PANEL_NOTIFICATION_TRIGGER, - title: 'Panel notifications', - description: 'Actions appear in top-right corner of a panel.', + title: i18n.translate('embeddableApi.panelNotificationTrigger.title', { + defaultMessage: 'Panel notifications', + }), + description: i18n.translate('embeddableApi.panelNotificationTrigger.description', { + defaultMessage: 'Actions appear in top-right corner of a panel.', + }), }; + +export const isValueClickTriggerContext = ( + context: ChartActionContext +): context is ValueClickContext => context.data && 'data' in context.data; + +export const isRangeSelectTriggerContext = ( + context: ChartActionContext +): context is RangeSelectContext => context.data && 'range' in context.data; + +export const isContextMenuTriggerContext = (context: unknown): context is EmbeddableContext => + !!context && + typeof context === 'object' && + !!(context as EmbeddableContext).embeddable && + typeof (context as EmbeddableContext).embeddable === 'object'; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 84dd97c8288fc..e84dff1172c2e 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -17,6 +17,7 @@ import { CoreSetup as CoreSetup_2 } from 'src/core/public'; import { CoreSetup as CoreSetup_3 } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'kibana/public'; import * as CSS from 'csstype'; +import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; import { EmbeddableStart as EmbeddableStart_2 } from 'src/plugins/embeddable/public/plugin'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; @@ -425,6 +426,7 @@ export type EmbeddableInput = { timeRange?: TimeRange; query?: Query; filters?: Filter[]; + searchSessionId?: string; }; // Warning: (ae-missing-release-tag) "EmbeddableInstanceConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -693,6 +695,11 @@ export interface IEmbeddable): void; } +// Warning: (ae-missing-release-tag) "isContextMenuTriggerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isContextMenuTriggerContext: (context: unknown) => context is EmbeddableContext; + // Warning: (ae-missing-release-tag) "isErrorEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -882,7 +889,7 @@ export const withEmbeddableSubscription: { + const encodedString = 'test%3F'; + expect(attemptToURIDecode(encodedString)).toBe('test?'); +}); + +// react router partially decodes %25 sequence to % in match params +// https://github.com/elastic/kibana/pull/81664 +test('ignores the error if a string is already decoded', () => { + const decodedString = 'test%'; + expect(attemptToURIDecode(decodedString)).toBe(decodedString); +}); diff --git a/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.ts b/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.ts new file mode 100644 index 0000000000000..65444b83f77bb --- /dev/null +++ b/src/plugins/es_ui_shared/public/url/attempt_to_uri_decode.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * Use this function with any match params coming from react router to safely decode values. + * https://github.com/elastic/kibana/pull/81664 + */ +export const attemptToURIDecode = (value: string) => { + let result = value; + try { + result = decodeURIComponent(value); + } catch (e) { + // do nothing + } + return result; +}; diff --git a/src/plugins/es_ui_shared/public/url/index.ts b/src/plugins/es_ui_shared/public/url/index.ts index 692e094f9eda4..a40885545ca0c 100644 --- a/src/plugins/es_ui_shared/public/url/index.ts +++ b/src/plugins/es_ui_shared/public/url/index.ts @@ -18,3 +18,4 @@ */ export { extractQueryParams } from './extract_query_params'; +export { attemptToURIDecode } from './attempt_to_uri_decode'; diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index a658d3457407c..308d6f7e71814 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -22,6 +22,7 @@ import * as expressionTypes from '../expression_types'; import * as expressionFunctions from '../expression_functions'; import { Execution } from '../execution'; import { ExpressionAstFunction, parseExpression } from '../ast'; +import { MigrateFunction } from '../../../kibana_utils/common/persistable_state'; describe('Executor', () => { test('can instantiate', () => { @@ -158,6 +159,7 @@ describe('Executor', () => { const injectFn = jest.fn().mockImplementation((args, references) => args); const extractFn = jest.fn().mockReturnValue({ args: {}, references: [] }); + const migrateFn = jest.fn().mockImplementation((args) => args); const fooFn = { name: 'foo', @@ -174,6 +176,14 @@ describe('Executor', () => { inject: (state: ExpressionAstFunction['arguments']) => { return injectFn(state); }, + migrations: { + '7.10.0': (((state: ExpressionAstFunction, version: string): ExpressionAstFunction => { + return migrateFn(state, version); + }) as any) as MigrateFunction, + '7.10.1': (((state: ExpressionAstFunction, version: string): ExpressionAstFunction => { + return migrateFn(state, version); + }) as any) as MigrateFunction, + }, fn: jest.fn(), }; executor.registerFunction(fooFn); @@ -194,5 +204,26 @@ describe('Executor', () => { expect(extractFn).toBeCalledTimes(5); }); }); + + describe('.migrate', () => { + test('calls migrate function for every expression function in expression', () => { + executor.migrate( + parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'), + '7.10.0' + ); + expect(migrateFn).toBeCalledTimes(5); + }); + }); + + describe('.migrateToLatest', () => { + test('calls extract function for every expression function in expression', () => { + migrateFn.mockClear(); + executor.migrateToLatest( + parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'), + '7.10.0' + ); + expect(migrateFn).toBeCalledTimes(10); + }); + }); }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 85b5589b593af..19fc4cf5a14a2 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -32,7 +32,7 @@ import { typeSpecs } from '../expression_types/specs'; import { functionSpecs } from '../expression_functions/specs'; import { getByAlias } from '../util'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState } from '../../../kibana_utils/common'; +import { PersistableState, SerializableState } from '../../../kibana_utils/common'; import { ExpressionExecutionParams } from '../service'; export interface ExpressionExecOptions { @@ -88,6 +88,20 @@ export class FunctionsRegistry implements IRegistry { } } +const semverGte = (semver1: string, semver2: string) => { + const regex = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/; + const matches1 = regex.exec(semver1) as RegExpMatchArray; + const matches2 = regex.exec(semver2) as RegExpMatchArray; + + const [, major1, minor1, patch1] = matches1; + const [, major2, minor2, patch2] = matches2; + + return ( + major1 > major2 || + (major1 === major2 && (minor1 > minor2 || (minor1 === minor2 && patch1 >= patch2))) + ); +}; + export class Executor = Record> implements PersistableState { static createWithDefaults = Record>( @@ -249,6 +263,27 @@ export class Executor = Record { + if (!fn.migrations[version]) return link; + const updatedAst = fn.migrations[version](link) as ExpressionAstFunction; + link.arguments = updatedAst.arguments; + link.type = updatedAst.type; + }); + } + + public migrateToLatest(ast: unknown, version: string) { + return this.walkAst(cloneDeep(ast) as ExpressionAstExpression, (fn, link) => { + for (const key of Object.keys(fn.migrations)) { + if (semverGte(key, version)) { + const updatedAst = fn.migrations[key](link) as ExpressionAstFunction; + link.arguments = updatedAst.arguments; + link.type = updatedAst.type; + } + } + }); + } + public fork(): Executor { const initialState = this.state.get(); const fork = new Executor(initialState); diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts index 0b56d3c169ff4..2879cc8e3632c 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -24,7 +24,7 @@ import { ExpressionValue } from '../expression_types/types'; import { ExecutionContext } from '../execution'; import { ExpressionAstFunction } from '../ast'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState } from '../../../kibana_utils/common'; +import { PersistableState, SerializableState } from '../../../kibana_utils/common'; export class ExpressionFunction implements PersistableState { /** @@ -76,6 +76,9 @@ export class ExpressionFunction implements PersistableState ExpressionAstFunction['arguments']; + migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; constructor(functionDefinition: AnyExpressionFunctionDefinition) { const { @@ -91,6 +94,7 @@ export class ExpressionFunction implements PersistableState c); this.inject = inject || identity; this.extract = extract || ((s) => ({ state: s, references: [] })); + this.migrations = migrations || {}; for (const [key, arg] of Object.entries(args || {})) { this.args[key] = new ExpressionFunctionParameter(key, arg); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index abbba433ab3ca..0f898563c3d0e 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -24,7 +24,7 @@ import { ExecutionContract } from '../execution/execution_contract'; import { AnyExpressionTypeDefinition } from '../expression_types'; import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState } from '../../../kibana_utils/common'; +import { PersistableState, SerializableState } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common/adapters'; import { ExecutionContextSearch } from '../execution'; @@ -303,6 +303,26 @@ export class ExpressionsService implements PersistableState { + return this.executor.migrate(state, version); + }; + + /** + * Migrates expression to the latest version + * @param state expression AST to update + * @param version the version of kibana in which expression was created + * @returns migrated expression AST + */ + public readonly migrateToLatest = (state: unknown, version: string) => { + return this.executor.migrateToLatest(state, version); + }; + /** * Returns Kibana Platform *setup* life-cycle contract. Useful to return the * same contract on server-side and browser-side. diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index fe95cf5eb0cda..5ca085ebb0d1d 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -222,6 +222,12 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) @@ -342,6 +348,10 @@ export class ExpressionFunction implements PersistableState ExpressionAstFunction['arguments']; inputTypes: string[] | undefined; + // (undocumented) + migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; name: string; // (undocumented) telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; @@ -586,6 +596,8 @@ export class ExpressionsService implements PersistableState ReturnType; readonly inject: (state: ExpressionAstExpression, references: SavedObjectReference[]) => ExpressionAstExpression; + readonly migrate: (state: SerializableState, version: string) => ExpressionAstExpression; + readonly migrateToLatest: (state: unknown, version: string) => ExpressionAstExpression; readonly registerFunction: (functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)) => void; // (undocumented) readonly registerRenderer: (definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)) => void; @@ -1039,7 +1051,7 @@ export interface Range { // Warning: (ae-missing-release-tag) "ReactExpressionRenderer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element; +export const ReactExpressionRenderer: ({ className, dataAttrs, padding, renderError, expression, onEvent, reload$, debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => JSX.Element; // Warning: (ae-missing-release-tag) "ReactExpressionRendererProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1050,6 +1062,8 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { // (undocumented) dataAttrs?: string[]; // (undocumented) + debounce?: number; + // (undocumented) expression: string | ExpressionAstExpression; // (undocumented) onEvent?: (event: ExpressionRendererEvent) => void; @@ -1147,7 +1161,6 @@ export type UnmappedTypeStrings = 'date' | 'filter'; // // src/plugins/expressions/common/ast/types.ts:40:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts // src/plugins/expressions/common/expression_types/specs/error.ts:31:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts -// src/plugins/expressions/common/expression_types/specs/error.ts:32:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx index 7c1711f056d69..052c2a9f6a24a 100644 --- a/src/plugins/expressions/public/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -113,6 +113,39 @@ describe('ExpressionRenderer', () => { instance.unmount(); }); + it('waits for debounce period if specified', () => { + jest.useFakeTimers(); + + const refreshSubject = new Subject(); + const loaderUpdate = jest.fn(); + + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$: new Subject(), + data$: new Subject(), + loading$: new Subject(), + update: loaderUpdate, + destroy: jest.fn(), + }; + }); + + const instance = mount( + + ); + + instance.setProps({ expression: 'abc' }); + + expect(loaderUpdate).toHaveBeenCalledTimes(1); + + act(() => { + jest.runAllTimers(); + }); + + expect(loaderUpdate).toHaveBeenCalledTimes(2); + + instance.unmount(); + }); + it('should display a custom error message if the user provides one and then remove it after successful render', () => { const dataSubject = new Subject(); const data$ = dataSubject.asObservable().pipe(share()); diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 99d170c96666d..fecebf36ab7e6 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -45,6 +45,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { * An observable which can be used to re-run the expression without destroying the component */ reload$?: Observable; + debounce?: number; } export type ReactExpressionRendererType = React.ComponentType; @@ -71,6 +72,7 @@ export const ReactExpressionRenderer = ({ expression, onEvent, reload$, + debounce, ...expressionLoaderOptions }: ReactExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); @@ -85,12 +87,28 @@ export const ReactExpressionRenderer = ({ const errorRenderHandlerRef: React.MutableRefObject = useRef( null ); + const [debouncedExpression, setDebouncedExpression] = useState(expression); + useEffect(() => { + if (debounce === undefined) { + return; + } + const handler = setTimeout(() => { + setDebouncedExpression(expression); + }, debounce); + + return () => { + clearTimeout(handler); + }; + }, [expression, debounce]); + + const activeExpression = debounce !== undefined ? debouncedExpression : expression; + const waitingForDebounceToComplete = debounce !== undefined && expression !== debouncedExpression; /* eslint-disable react-hooks/exhaustive-deps */ // OK to ignore react-hooks/exhaustive-deps because options update is handled by calling .update() useEffect(() => { const subs: Subscription[] = []; - expressionLoaderRef.current = new ExpressionLoader(mountpoint.current!, expression, { + expressionLoaderRef.current = new ExpressionLoader(mountpoint.current!, activeExpression, { ...expressionLoaderOptions, // react component wrapper provides different // error handling api which is easier to work with from react @@ -146,21 +164,21 @@ export const ReactExpressionRenderer = ({ useEffect(() => { const subscription = reload$?.subscribe(() => { if (expressionLoaderRef.current) { - expressionLoaderRef.current.update(expression, expressionLoaderOptions); + expressionLoaderRef.current.update(activeExpression, expressionLoaderOptions); } }); return () => subscription?.unsubscribe(); - }, [reload$, expression, ...Object.values(expressionLoaderOptions)]); + }, [reload$, activeExpression, ...Object.values(expressionLoaderOptions)]); // Re-fetch data automatically when the inputs change useShallowCompareEffect( () => { if (expressionLoaderRef.current) { - expressionLoaderRef.current.update(expression, expressionLoaderOptions); + expressionLoaderRef.current.update(activeExpression, expressionLoaderOptions); } }, // when expression is changed by reference and when any other loaderOption is changed by reference - [{ expression, ...expressionLoaderOptions }] + [{ activeExpression, ...expressionLoaderOptions }] ); /* eslint-enable react-hooks/exhaustive-deps */ @@ -188,7 +206,9 @@ export const ReactExpressionRenderer = ({ return (
{state.isEmpty && } - {state.isLoading && } + {(state.isLoading || waitingForDebounceToComplete) && ( + + )} {!state.isLoading && state.error && renderError && diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index d6925a027358c..c8d5464929033 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -204,6 +204,12 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) @@ -314,6 +320,10 @@ export class ExpressionFunction implements PersistableState ExpressionAstFunction['arguments']; inputTypes: string[] | undefined; + // (undocumented) + migrations: { + [key: string]: (state: SerializableState) => SerializableState; + }; name: string; // (undocumented) telemetry: (state: ExpressionAstFunction['arguments'], telemetryData: Record) => Record; @@ -939,7 +949,6 @@ export type UnmappedTypeStrings = 'date' | 'filter'; // // src/plugins/expressions/common/ast/types.ts:40:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts // src/plugins/expressions/common/expression_types/specs/error.ts:31:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts -// src/plugins/expressions/common/expression_types/specs/error.ts:32:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx b/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx index 13b70383147eb..bbc7f28235d6a 100644 --- a/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx @@ -25,8 +25,8 @@ import { SolutionPanel } from './solution_panel'; import { FeatureCatalogueEntry, FeatureCatalogueSolution } from '../../../'; const sortByOrder = ( - { order: orderA = 0 }: FeatureCatalogueSolution, - { order: orderB = 0 }: FeatureCatalogueSolution + { order: orderA = 0 }: FeatureCatalogueSolution | FeatureCatalogueEntry, + { order: orderB = 0 }: FeatureCatalogueSolution | FeatureCatalogueEntry ) => orderA - orderB; interface Props { @@ -38,7 +38,9 @@ interface Props { export const SolutionsSection: FC = ({ addBasePath, solutions, directories }) => { // Separate Kibana from other solutions const kibana = solutions.find(({ id }) => id === 'kibana'); - const kibanaApps = directories.filter(({ solutionId }) => solutionId === 'kibana'); + const kibanaApps = directories + .filter(({ solutionId }) => solutionId === 'kibana') + .sort(sortByOrder); solutions = solutions.sort(sortByOrder).filter(({ id }) => id !== 'kibana'); return ( diff --git a/src/plugins/home/public/services/environment/environment.mock.ts b/src/plugins/home/public/services/environment/environment.mock.ts index d8be02bf6552c..7b1fb0a85e0d9 100644 --- a/src/plugins/home/public/services/environment/environment.mock.ts +++ b/src/plugins/home/public/services/environment/environment.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { EnvironmentService, EnvironmentServiceSetup } from './environment'; const createSetupMock = (): jest.Mocked => { diff --git a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts index e1a415ba2d571..6cb4bb1db90a7 100644 --- a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts +++ b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { FeatureCatalogueRegistrySetup, FeatureCatalogueRegistry, diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts index 667730e25a2e3..7aa1aec0898fe 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { TutorialService, TutorialServiceSetup } from './tutorial_service'; const createSetupMock = (): jest.Mocked => { diff --git a/src/plugins/home/server/services/sample_data/sample_data_registry.mock.ts b/src/plugins/home/server/services/sample_data/sample_data_registry.mock.ts index 4d0fb4f96023a..717f21576def9 100644 --- a/src/plugins/home/server/services/sample_data/sample_data_registry.mock.ts +++ b/src/plugins/home/server/services/sample_data/sample_data_registry.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SampleDataRegistrySetup, SampleDataRegistryStart, diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.mock.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.mock.ts index 5ff0152062f4b..162216161342c 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.mock.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { TutorialsRegistrySetup, TutorialsRegistryStart, diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts index b91a265da7d18..96dab89bb1702 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { TutorialsRegistry } from './tutorials_registry'; import { coreMock } from '../../../../../core/server/mocks'; import { CoreSetup } from '../../../../../core/server'; diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts b/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts index 2c2c68b8ead2d..5ddbca67d9fec 100644 --- a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts +++ b/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { EnvironmentService, EnvironmentServiceSetup } from './environment'; import { MlCardState } from '../../types'; diff --git a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts index 5de6ddf351c02..9d4c1bf005e63 100644 --- a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts +++ b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { CoreSetup, RequestHandlerContext } from 'src/core/server'; import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; import { registerPreviewScriptedFieldRoute } from './preview_scripted_field'; diff --git a/src/plugins/inspector/common/adapters/request/types.ts b/src/plugins/inspector/common/adapters/request/types.ts index 0a62df7c8d10c..c08c2ef6f706f 100644 --- a/src/plugins/inspector/common/adapters/request/types.ts +++ b/src/plugins/inspector/common/adapters/request/types.ts @@ -49,6 +49,7 @@ export interface Request extends RequestParams { export interface RequestParams { id?: string; description?: string; + searchSessionId?: string; } export interface RequestStatistics { diff --git a/src/plugins/inspector/public/views/requests/components/requests_view.tsx b/src/plugins/inspector/public/views/requests/components/requests_view.tsx index a433ea70dc35c..13575de0c5064 100644 --- a/src/plugins/inspector/public/views/requests/components/requests_view.tsx +++ b/src/plugins/inspector/public/views/requests/components/requests_view.tsx @@ -153,6 +153,21 @@ export class RequestsViewComponent extends Component )} + {this.state.request && this.state.request.searchSessionId && ( + +

+ +

+
+ )} + {this.state.request && } diff --git a/src/plugins/inspector/tsconfig.json b/src/plugins/inspector/tsconfig.json new file mode 100644 index 0000000000000..2a9c41464532c --- /dev/null +++ b/src/plugins/inspector/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "index.ts"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" } + ] +} diff --git a/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts new file mode 100644 index 0000000000000..3d1dcdbef3f4b --- /dev/null +++ b/src/plugins/kibana_legacy/public/angular/kbn_top_nav.d.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; + +export const createTopNavDirective: Injectable>; +export const createTopNavHelper: ( + options: unknown +) => Injectable>; +export function loadKbnTopNavDirectives(navUi: unknown): void; diff --git a/src/plugins/vis_type_vislib/public/components/options/index.ts b/src/plugins/kibana_legacy/public/angular/promises.d.ts similarity index 77% rename from src/plugins/vis_type_vislib/public/components/options/index.ts rename to src/plugins/kibana_legacy/public/angular/promises.d.ts index 57afbd4818ae4..1a2ce66834d7b 100644 --- a/src/plugins/vis_type_vislib/public/components/options/index.ts +++ b/src/plugins/kibana_legacy/public/angular/promises.d.ts @@ -17,8 +17,4 @@ * under the License. */ -export { GaugeOptions } from './gauge'; -export { PieOptions } from './pie'; -export { PointSeriesOptions } from './point_series'; -export { HeatmapOptions } from './heatmap'; -export { MetricsAxisOptions } from './metrics_axes'; +export function PromiseServiceCreator($q: unknown, $timeout: unknown): (fn: unknown) => unknown; diff --git a/src/plugins/vis_type_vega/public/components/index.ts b/src/plugins/kibana_legacy/public/angular/watch_multi.d.ts similarity index 92% rename from src/plugins/vis_type_vega/public/components/index.ts rename to src/plugins/kibana_legacy/public/angular/watch_multi.d.ts index 90f067c778fd2..7c73abf2f9aa2 100644 --- a/src/plugins/vis_type_vega/public/components/index.ts +++ b/src/plugins/kibana_legacy/public/angular/watch_multi.d.ts @@ -17,4 +17,4 @@ * under the License. */ -export { VegaVisEditor } from './vega_vis_editor'; +export function watchMultiDecorator($provide: unknown): void; diff --git a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts new file mode 100644 index 0000000000000..e4ef43fe8d443 --- /dev/null +++ b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.d.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; + +export const KbnAccessibleClickProvider: Injectable>; diff --git a/src/plugins/kibana_legacy/public/utils/private.d.ts b/src/plugins/kibana_legacy/public/utils/private.d.ts index 3efc9cd5308f7..00b0220316ead 100644 --- a/src/plugins/kibana_legacy/public/utils/private.d.ts +++ b/src/plugins/kibana_legacy/public/utils/private.d.ts @@ -17,4 +17,8 @@ * under the License. */ +import { IServiceProvider } from 'angular'; + export type IPrivate = (provider: (...injectable: any[]) => T) => T; + +export function PrivateProvider(): IServiceProvider; diff --git a/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts b/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts new file mode 100644 index 0000000000000..eff9b4b871f56 --- /dev/null +++ b/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function registerListenEventListener($rootScope: unknown): void; diff --git a/src/plugins/kibana_overview/public/plugin.ts b/src/plugins/kibana_overview/public/plugin.ts index 663058ad16ec8..1139002dc19b6 100644 --- a/src/plugins/kibana_overview/public/plugin.ts +++ b/src/plugins/kibana_overview/public/plugin.ts @@ -109,7 +109,7 @@ export class KibanaOverviewPlugin defaultMessage: 'Search and find insights.', }), i18n.translate('kibanaOverview.kibana.appDescription3', { - defaultMessage: 'Design pixel-perfect reports.', + defaultMessage: 'Design pixel-perfect presentations.', }), i18n.translate('kibanaOverview.kibana.appDescription4', { defaultMessage: 'Plot geographic data.', diff --git a/src/plugins/kibana_utils/common/persistable_state/index.ts b/src/plugins/kibana_utils/common/persistable_state/index.ts index ae5e3d514554c..40feea3f24f28 100644 --- a/src/plugins/kibana_utils/common/persistable_state/index.ts +++ b/src/plugins/kibana_utils/common/persistable_state/index.ts @@ -27,6 +27,11 @@ export type SerializableState = { [key: string]: Serializable; }; +export type MigrateFunction< + FromVersion extends SerializableState = SerializableState, + ToVersion extends SerializableState = SerializableState +> = (state: FromVersion) => ToVersion; + export interface PersistableState

{ /** * function to extract telemetry information @@ -47,8 +52,29 @@ export interface PersistableState

{ state: P; references: SavedObjectReference[] }; + + /** + * migrateToLatest function receives state of older version and should migrate to the latest version + * @param state + * @param version + */ + migrateToLatest?: (state: SerializableState, version: string) => P; + + /** + * migrate function runs the specified migration + * @param state + * @param version + */ + migrate?: (state: SerializableState, version: string) => SerializableState; } export type PersistableStateDefinition

= Partial< - PersistableState

->; + Omit, 'migrate'> +> & { + /** + * list of all migrations per semver + */ + migrations?: { + [key: string]: MigrateFunction; + }; +}; diff --git a/src/plugins/kibana_utils/public/storage/storage.test.ts b/src/plugins/kibana_utils/public/storage/storage.test.ts index 8c5d3d11a21fe..293194cae55a6 100644 --- a/src/plugins/kibana_utils/public/storage/storage.test.ts +++ b/src/plugins/kibana_utils/public/storage/storage.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { Storage } from './storage'; import { IStorage, IStorageWrapper } from './types'; diff --git a/src/plugins/maps_legacy/README.md b/src/plugins/maps_legacy/README.md new file mode 100644 index 0000000000000..4a870e4f7492d --- /dev/null +++ b/src/plugins/maps_legacy/README.md @@ -0,0 +1,7 @@ +# 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/region_map/README.md b/src/plugins/region_map/README.md new file mode 100644 index 0000000000000..540ab47c102d3 --- /dev/null +++ b/src/plugins/region_map/README.md @@ -0,0 +1,5 @@ +# 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/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 3a03c5c01b3c2..ea86ea58faf61 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -101,8 +101,11 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` }, ] } + onTableChange={[Function]} pagination={ Object { + "pageIndex": 0, + "pageSize": 5, "pageSizeOptions": Array [ 5, 10, @@ -246,6 +249,10 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "newIndexPatternId": "2", }, ], + "unmatchedReferencesTablePagination": Object { + "pageIndex": 0, + "pageSize": 5, + }, }, }, ], @@ -403,8 +410,11 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` }, ] } + onTableChange={[Function]} pagination={ Object { + "pageIndex": 0, + "pageSize": 5, "pageSizeOptions": Array [ 5, 10, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 3165ea4ca0794..75792becc29d8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -88,6 +88,7 @@ export interface FlyoutState { conflictedSavedObjectsLinkedToSavedSearches?: any[]; conflictedSearchDocs?: any[]; unmatchedReferences?: ProcessedImportResponse['unmatchedReferences']; + unmatchedReferencesTablePagination: { pageIndex: number; pageSize: number }; failedImports?: ProcessedImportResponse['failedImports']; successfulImports?: ProcessedImportResponse['successfulImports']; conflictingRecord?: ConflictingRecord; @@ -115,6 +116,10 @@ export class Flyout extends Component { conflictedSavedObjectsLinkedToSavedSearches: undefined, conflictedSearchDocs: undefined, unmatchedReferences: undefined, + unmatchedReferencesTablePagination: { + pageIndex: 0, + pageSize: 5, + }, conflictingRecord: undefined, error: undefined, file: undefined, @@ -467,7 +472,7 @@ export class Flyout extends Component { }; renderUnmatchedReferences() { - const { unmatchedReferences } = this.state; + const { unmatchedReferences, unmatchedReferencesTablePagination: tablePagination } = this.state; if (!unmatchedReferences) { return null; @@ -527,22 +532,28 @@ export class Flyout extends Component { { defaultMessage: 'New index pattern' } ), render: (id: string) => { - const options = this.state.indexPatterns!.map( - (indexPattern) => - ({ - text: indexPattern.title, - value: indexPattern.id, - 'data-test-subj': `indexPatternOption-${indexPattern.title}`, - } as { text: string; value: string; 'data-test-subj'?: string }) - ); - - options.unshift({ - text: '-- Skip Import --', - value: '', - }); + const options = [ + { + text: '-- Skip Import --', + value: '', + }, + ...this.state.indexPatterns!.map( + (indexPattern) => + ({ + text: indexPattern.title, + value: indexPattern.id, + 'data-test-subj': `indexPatternOption-${indexPattern.title}`, + } as { text: string; value: string; 'data-test-subj'?: string }) + ), + ]; + + const selectedValue = + unmatchedReferences?.find((unmatchedRef) => unmatchedRef.existingIndexPatternId === id) + ?.newIndexPatternId ?? ''; return ( this.onIndexChanged(id, e)} options={options} @@ -553,6 +564,7 @@ export class Flyout extends Component { ]; const pagination = { + ...tablePagination, pageSizeOptions: [5, 10, 25], }; @@ -561,6 +573,16 @@ export class Flyout extends Component { items={unmatchedReferences as any[]} columns={columns} pagination={pagination} + onTableChange={({ page }) => { + if (page) { + this.setState({ + unmatchedReferencesTablePagination: { + pageSize: page.size, + pageIndex: page.index, + }, + }); + } + }} /> ); } diff --git a/src/plugins/saved_objects_management/public/services/action_service.mock.ts b/src/plugins/saved_objects_management/public/services/action_service.mock.ts index 97c95a589b925..dfa5f47325eb6 100644 --- a/src/plugins/saved_objects_management/public/services/action_service.mock.ts +++ b/src/plugins/saved_objects_management/public/services/action_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsManagementActionService, SavedObjectsManagementActionServiceSetup, diff --git a/src/plugins/saved_objects_management/public/services/column_service.mock.ts b/src/plugins/saved_objects_management/public/services/column_service.mock.ts index 977b2099771ba..9dfa130d463c5 100644 --- a/src/plugins/saved_objects_management/public/services/column_service.mock.ts +++ b/src/plugins/saved_objects_management/public/services/column_service.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsManagementColumnService, SavedObjectsManagementColumnServiceSetup, diff --git a/src/plugins/saved_objects_management/public/services/service_registry.ts b/src/plugins/saved_objects_management/public/services/service_registry.ts index 2d6ec0b92047a..d4dc6d6166e46 100644 --- a/src/plugins/saved_objects_management/public/services/service_registry.ts +++ b/src/plugins/saved_objects_management/public/services/service_registry.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectLoader } from '../../../saved_objects/public'; export interface SavedObjectsManagementServiceRegistryEntry { diff --git a/src/plugins/saved_objects_management/server/services/management.mock.ts b/src/plugins/saved_objects_management/server/services/management.mock.ts index 85c2d3e4b08d9..d765607316591 100644 --- a/src/plugins/saved_objects_management/server/services/management.mock.ts +++ b/src/plugins/saved_objects_management/server/services/management.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsManagement } from './management'; type Management = PublicMethodsOf; diff --git a/src/plugins/saved_objects_management/server/services/management.ts b/src/plugins/saved_objects_management/server/services/management.ts index 499f37990c346..f24226d798fae 100644 --- a/src/plugins/saved_objects_management/server/services/management.ts +++ b/src/plugins/saved_objects_management/server/services/management.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ISavedObjectTypeRegistry, SavedObject } from 'src/core/server'; export type ISavedObjectsManagement = PublicMethodsOf; diff --git a/src/plugins/security_oss/public/plugin.mock.ts b/src/plugins/security_oss/public/plugin.mock.ts index c513d241dccbb..bb6e1c6bc6545 100644 --- a/src/plugins/security_oss/public/plugin.mock.ts +++ b/src/plugins/security_oss/public/plugin.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { mockInsecureClusterService } from './insecure_cluster_service/insecure_cluster_service.mock'; import { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin'; diff --git a/src/plugins/share/public/services/share_menu_manager.mock.ts b/src/plugins/share/public/services/share_menu_manager.mock.ts index 7104abeb26090..c20543fea16db 100644 --- a/src/plugins/share/public/services/share_menu_manager.mock.ts +++ b/src/plugins/share/public/services/share_menu_manager.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ShareMenuManager, ShareMenuManagerStart } from './share_menu_manager'; const createStartMock = (): jest.Mocked => { diff --git a/src/plugins/share/public/services/share_menu_registry.mock.ts b/src/plugins/share/public/services/share_menu_registry.mock.ts index b69032f0b3e09..2645b7a73fdfd 100644 --- a/src/plugins/share/public/services/share_menu_registry.mock.ts +++ b/src/plugins/share/public/services/share_menu_registry.mock.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ShareMenuRegistry, ShareMenuRegistrySetup, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 160e99a40790c..c840cbe8fc94d 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -16,13 +16,13 @@ "search": { "properties": { "successCount": { - "type": "number" + "type": "long" }, "errorCount": { - "type": "number" + "type": "long" }, "averageDuration": { - "type": "long" + "type": "float" } } }, diff --git a/src/plugins/tile_map/README.md b/src/plugins/tile_map/README.md new file mode 100644 index 0000000000000..633ee7dba46d6 --- /dev/null +++ b/src/plugins/tile_map/README.md @@ -0,0 +1,5 @@ +# 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/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts index f83cc97c2a8ef..87a1df959ccbd 100644 --- a/src/plugins/ui_actions/public/plugin.ts +++ b/src/plugins/ui_actions/public/plugin.ts @@ -18,6 +18,7 @@ */ import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { UiActionsService } from './service'; import { selectRangeTrigger, diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index 229997281db49..8393f7480d4e4 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -12,6 +12,7 @@ import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; import { Plugin } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; +import { PublicMethodsOf } from '@kbn/utility-types'; import React from 'react'; import * as Rx from 'rxjs'; import { UiComponent } from 'src/plugins/kibana_utils/public'; diff --git a/src/plugins/url_forwarding/tsconfig.json b/src/plugins/url_forwarding/tsconfig.json new file mode 100644 index 0000000000000..8e867a6bad14f --- /dev/null +++ b/src/plugins/url_forwarding/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../kibana_legacy/tsconfig.json" } + ] +} diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 430241cbe0a05..5a853972d34a8 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -138,7 +138,7 @@ The `schema` field is a proscribed data model assists with detecting changes in The `AllowedSchemaTypes` is the list of allowed schema types for the usage fields getting reported: ``` -'keyword', 'text', 'number', 'boolean', 'long', 'date', 'float' +'long', 'integer', 'short', 'byte', 'double', 'float', 'keyword', 'text', 'boolean', 'date' ``` ### Arrays @@ -171,7 +171,7 @@ export const myCollector = makeUsageCollector({ }, some_obj: { total: { - type: 'number', + type: 'long', }, }, some_array: { @@ -182,7 +182,7 @@ export const myCollector = makeUsageCollector({ type: 'array', items: { total: { - type: 'number', + type: 'long', }, }, }, diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 951418d448cbd..73febc0183fc5 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -27,14 +27,9 @@ import { export type CollectorFormatForBulkUpload = (result: T) => { type: string; payload: U }; -export type AllowedSchemaTypes = - | 'keyword' - | 'text' - | 'number' - | 'boolean' - | 'long' - | 'date' - | 'float'; +export type AllowedSchemaNumberTypes = 'long' | 'integer' | 'short' | 'byte' | 'double' | 'float'; + +export type AllowedSchemaTypes = AllowedSchemaNumberTypes | 'keyword' | 'text' | 'boolean' | 'date'; export interface SchemaField { type: string; diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index da85f9ab181c9..2f8be884a8a7b 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -21,6 +21,7 @@ export { CollectorSet } from './collector_set'; export { Collector, AllowedSchemaTypes, + AllowedSchemaNumberTypes, SchemaField, MakeSchemaFrom, CollectorOptions, diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts index 834ad8b70ad0d..a56155db02f6b 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts @@ -33,6 +33,7 @@ import { FieldParamEditor, OrderByParamEditor } from './controls'; import { EditorConfig } from './utils'; import { Schema } from '../schemas'; import { EditorVisState } from './sidebar/state/reducers'; +import { groupAndSortBy } from '../utils'; jest.mock('../utils', () => ({ groupAndSortBy: jest.fn(() => ['indexedFields']), @@ -169,6 +170,9 @@ describe('DefaultEditorAggParams helpers', () => { ], advanced: [], }); + + // Should be grouped using displayName as label + expect(groupAndSortBy).toHaveBeenCalledWith(expect.anything(), 'type', 'displayName', 'name'); }); }); diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index b13ca32601aa9..271fc75a0853e 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -93,7 +93,7 @@ function getAggParamsToRender({ } } fields = filterAggTypeFields(availableFields, agg); - indexedFields = groupAndSortBy(fields, 'type', 'name'); + indexedFields = groupAndSortBy(fields, 'type', 'displayName', 'name'); if (fields && !indexedFields.length && index > 0) { // don't draw the rest of the options if there are no indexed fields and it's an extra param (index > 0). diff --git a/src/plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx index bfc4f881f8458..41d6db25da5e2 100644 --- a/src/plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx @@ -52,7 +52,7 @@ function FieldParamEditor({ }: FieldParamEditorProps) { const [isDirty, setIsDirty] = useState(false); const selectedOptions: ComboBoxGroupedOptions = value - ? [{ label: value.displayName || value.name, target: value }] + ? [{ label: value.displayName, target: value, key: value.name }] : []; const onChange = (options: EuiComboBoxOptionOption[]) => { diff --git a/src/plugins/vis_default_editor/public/utils.ts b/src/plugins/vis_default_editor/public/utils.ts index d0a9c067e9da2..11b7c07acc2c2 100644 --- a/src/plugins/vis_default_editor/public/utils.ts +++ b/src/plugins/vis_default_editor/public/utils.ts @@ -18,10 +18,12 @@ */ interface ComboBoxOption { + key?: string; label: string; target: T; } interface ComboBoxGroupedOption { + key?: string; label: string; options: Array>; } @@ -40,15 +42,22 @@ export type ComboBoxGroupedOptions = Array>; * @returns An array of grouped and sorted alphabetically `objects` that are compatible with EuiComboBox options. */ export function groupAndSortBy< - T extends Record, + T extends Record, TGroupBy extends string = 'type', - TLabelName extends string = 'title' ->(objects: T[], groupBy: TGroupBy, labelName: TLabelName): ComboBoxGroupedOptions { + TLabelName extends string = 'title', + TKeyName extends string = never +>( + objects: T[], + groupBy: TGroupBy, + labelName: TLabelName, + keyName?: TKeyName +): ComboBoxGroupedOptions { const groupedOptions = objects.reduce((array, obj) => { const group = array.find((element) => element.label === obj[groupBy]); const option = { label: obj[labelName], target: obj, + ...(keyName ? { key: obj[keyName] } : {}), }; if (group && group.options) { diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx index cb0daa6d29382..a14328ac994f0 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx @@ -45,7 +45,9 @@ export const TagCloudChart = ({ const visController = useRef(null); useEffect(() => { - visController.current = new TagCloudVisualization(chartDiv.current, colors, fireEvent); + if (chartDiv.current) { + visController.current = new TagCloudVisualization(chartDiv.current, colors, fireEvent); + } return () => { visController.current.destroy(); visController.current = null; diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx index 04579407105e8..2c914d3c5b662 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx @@ -42,12 +42,6 @@ export const getTimelionVisRenderer: ( const [seriesList] = visData.sheet; const showNoResult = !seriesList || !seriesList.list.length; - if (showNoResult) { - // send the render complete event when there is no data to show - // to notify that a chart is updated - handlers.done(); - } - render( diff --git a/src/plugins/vis_type_timeseries/common/agg_lookup.js b/src/plugins/vis_type_timeseries/common/agg_lookup.js index 432da03e3d45d..0a71ab34082f8 100644 --- a/src/plugins/vis_type_timeseries/common/agg_lookup.js +++ b/src/plugins/vis_type_timeseries/common/agg_lookup.js @@ -98,7 +98,7 @@ export const lookup = { }), top_hit: i18n.translate('visTypeTimeseries.aggLookup.topHitLabel', { defaultMessage: 'Top Hit' }), positive_rate: i18n.translate('visTypeTimeseries.aggLookup.positiveRateLabel', { - defaultMessage: 'Positive Rate', + defaultMessage: 'Counter Rate', }), }; diff --git a/src/plugins/vis_type_timeseries/common/calculate_label.js b/src/plugins/vis_type_timeseries/common/calculate_label.js index 9f3030eeb6eae..96e9fa0825b25 100644 --- a/src/plugins/vis_type_timeseries/common/calculate_label.js +++ b/src/plugins/vis_type_timeseries/common/calculate_label.js @@ -72,7 +72,7 @@ export function calculateLabel(metric, metrics) { } if (metric.type === 'positive_rate') { return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', { - defaultMessage: 'Positive Rate of {field}', + defaultMessage: 'Counter Rate of {field}', values: { field: metric.field }, }); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx index 968fa5384e1d8..6c75e081429de 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx @@ -63,7 +63,7 @@ describe('TSVB AggSelect', () => { "value": "count", }, Object { - "label": "Positive Rate", + "label": "Counter Rate", "value": "positive_rate", }, Object { @@ -131,7 +131,7 @@ describe('TSVB AggSelect', () => { "value": "filter_ratio", }, Object { - "label": "Positive Rate", + "label": "Counter Rate", "value": "positive_rate", }, Object { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx index 7701d351e5478..5c8049e363694 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx @@ -54,7 +54,7 @@ const metricAggs: AggSelectOption[] = [ }, { label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.positiveRateLabel', { - defaultMessage: 'Positive Rate', + defaultMessage: 'Counter Rate', }), value: 'positive_rate', }, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss index c445d456a1703..9585711c73dd2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss @@ -3,29 +3,23 @@ flex-direction: column; flex: 1 1 100%; - // border used in lieu of padding to prevent overlapping background-color - border-width: $euiSizeS; - border-style: solid; - border-color: transparent; - .tvbVisTimeSeries { overflow: hidden; } .tvbVisTimeSeriesDark { .echReactiveChart_unavailable { - color: #DFE5EF; + color: #dfe5ef; } - .echLegendItem { - color: #DFE5EF; + .echLegendItem { + color: #dfe5ef; } } .tvbVisTimeSeriesLight { .echReactiveChart_unavailable { color: #343741; } - .echLegendItem { + .echLegendItem { color: #343741; } } } - diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index 2434285bd94c6..c12e518a9dcd3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -19,7 +19,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import reactCSS from 'reactcss'; import { startsWith, get, cloneDeep, map } from 'lodash'; import { htmlIdGenerator } from '@elastic/eui'; @@ -150,13 +149,6 @@ export class TimeseriesVisualization extends Component { render() { const { model, visData, onBrush } = this.props; - const styles = reactCSS({ - default: { - tvbVis: { - borderColor: get(model, 'background_color'), - }, - }, - }); const series = get(visData, `${model.id}.series`, []); const interval = getInterval(visData, model); const yAxisIdGenerator = htmlIdGenerator('yaxis'); @@ -231,7 +223,7 @@ export class TimeseriesVisualization extends Component { }); return ( -

+
void; + visData: VegaParser; +} + +type VegaVisController = InstanceType>; + +const VegaVisComponent = ({ visData, fireEvent, renderComplete, deps }: VegaVisComponentProps) => { + const chartDiv = useRef(null); + const visController = useRef(null); + + useEffect(() => { + if (chartDiv.current) { + const VegaVis = createVegaVisualization(deps); + visController.current = new VegaVis(chartDiv.current, fireEvent); + } + + return () => { + visController.current?.destroy(); + visController.current = null; + }; + }, [deps, fireEvent]); + + useEffect(() => { + if (visController.current) { + visController.current.render(visData).then(renderComplete); + } + }, [visData, renderComplete]); + + const updateChartSize = useMemo( + () => + throttle(() => { + if (visController.current) { + visController.current.render(visData).then(renderComplete); + } + }, 300), + [renderComplete, visData] + ); + + return ( + + {(resizeRef) => ( +
+
+
+ )} + + ); +}; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VegaVisComponent as default }; 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 5e770fcff556d..7d669235c36b8 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 @@ -30,6 +30,8 @@ import { VisParams } from '../vega_fn'; import { VegaHelpMenu } from './vega_help_menu'; import { VegaActionsMenu } from './vega_actions_menu'; +import './vega_editor.scss'; + const aceOptions = { maxLines: Infinity, highlightActiveLine: false, @@ -102,4 +104,6 @@ function VegaVisEditor({ stateParams, setValue }: VisOptionsProps) { ); } -export { VegaVisEditor }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VegaVisEditor as default }; diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_editor_lazy.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor_lazy.tsx new file mode 100644 index 0000000000000..d6c78972410e0 --- /dev/null +++ b/src/plugins/vis_type_vega/public/components/vega_vis_editor_lazy.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; + +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { VisParams } from '../vega_fn'; + +const VegaVisEditor = lazy(() => import('./vega_vis_editor')); + +export const VegaVisEditorComponent = (props: VisOptionsProps) => ( + +); diff --git a/src/plugins/vis_type_vega/public/index.scss b/src/plugins/vis_type_vega/public/index.scss deleted file mode 100644 index 78d9eb61999f7..0000000000000 --- a/src/plugins/vis_type_vega/public/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -// Prefix all styles with "vga" to avoid conflicts. -// Examples -// vgaChart -// vgaChart__legend -// vgaChart__legend--small -// vgaChart__legend-isLoading - -@import './vega_vis'; -@import './vega_editor'; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index ce5c5130961c6..04481685c841b 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -35,10 +35,10 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; import { IServiceSettings } from '../../maps_legacy/public'; -import './index.scss'; import { ConfigSchema } from '../config'; import { getVegaInspectorView } from './vega_inspector'; +import { getVegaVisRenderer } from './vega_vis_renderer'; /** @internal */ export interface VegaVisualizationDependencies { @@ -93,6 +93,7 @@ export class VegaPlugin implements Plugin, void> { inspector.registerView(getVegaInspectorView({ uiSettings: core.uiSettings })); expressions.registerFunction(() => createVegaFn(visualizationDependencies)); + expressions.registerRenderer(getVegaVisRenderer(visualizationDependencies)); visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); } diff --git a/src/plugins/vis_type_vega/public/to_ast.ts b/src/plugins/vis_type_vega/public/to_ast.ts new file mode 100644 index 0000000000000..a5fe8f13c3daf --- /dev/null +++ b/src/plugins/vis_type_vega/public/to_ast.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { Vis } from '../../visualizations/public'; +import { VegaExpressionFunctionDefinition, VisParams } from './vega_fn'; + +export const toExpressionAst = (vis: Vis) => { + const vega = buildExpressionFunction('vega', { + spec: vis.params.spec, + }); + + const ast = buildExpression([vega]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index c88b78948133c..25d4e76c336b3 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -40,21 +40,23 @@ interface Arguments { export type VisParams = Required; -interface RenderValue { +export interface RenderValue { visData: VegaParser; visType: 'vega'; visConfig: VisParams; } -export const createVegaFn = ( - dependencies: VegaVisualizationDependencies -): ExpressionFunctionDefinition< +export type VegaExpressionFunctionDefinition = ExpressionFunctionDefinition< 'vega', Input, Arguments, Output, ExecutionContext -> => ({ +>; + +export const createVegaFn = ( + dependencies: VegaVisualizationDependencies +): VegaExpressionFunctionDefinition => ({ name: 'vega', type: 'render', inputTypes: ['kibana_context', 'null'], @@ -80,7 +82,7 @@ export const createVegaFn = ( return { type: 'render', - as: 'visualization', + as: 'vega_vis', value: { visData: response, visType: 'vega', diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx index 6dfa7a23c4fe8..350e781dc7076 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx @@ -41,7 +41,7 @@ const specLabel = i18n.translate('visTypeVega.inspector.specLabel', { defaultMessage: 'Spec', }); -export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => { +const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => { const tabs = [ { id: 'data-viewer--id', @@ -75,3 +75,7 @@ export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => { /> ); }; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VegaDataInspector as default }; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx index 83d9e467646a6..4b3e48d6a37a0 100644 --- a/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx @@ -16,14 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'kibana/public'; -import { VegaAdapter } from './vega_adapter'; -import { VegaDataInspector, VegaDataInspectorProps } from './vega_data_inspector'; import { KibanaContextProvider } from '../../../kibana_react/public'; import { Adapters, RequestAdapter, InspectorViewDescription } from '../../../inspector/public'; +import { VegaAdapter } from './vega_adapter'; +import type { VegaDataInspectorProps } from './vega_data_inspector'; + +const VegaDataInspector = lazy(() => import('./vega_data_inspector')); export interface VegaInspectorAdapters extends Adapters { requests: RequestAdapter; @@ -46,7 +49,9 @@ export const getVegaInspectorView = (dependencies: VegaInspectorViewDependencies }, component: (props) => ( - + }> + + ), } as InspectorViewDescription); diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 0496f765e5e99..17f35b75f0016 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -21,22 +21,20 @@ import { i18n } from '@kbn/i18n'; import { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; -import { VegaVisEditor } from './components'; import { createVegaRequestHandler } from './vega_request_handler'; -// @ts-expect-error -import { createVegaVisualization } from './vega_visualization'; import { getDefaultSpec } from './default_spec'; import { createInspectorAdapters } from './vega_inspector'; import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; - +import { toExpressionAst } from './to_ast'; +import { VisParams } from './vega_fn'; import { getInfoMessage } from './components/experimental_map_vis_info'; +import { VegaVisEditorComponent } from './components/vega_vis_editor_lazy'; export const createVegaTypeDefinition = ( dependencies: VegaVisualizationDependencies -): BaseVisTypeOptions => { +): BaseVisTypeOptions => { const requestHandler = createVegaRequestHandler(dependencies); - const visualization = createVegaVisualization(dependencies); return { name: 'vega', @@ -49,13 +47,12 @@ export const createVegaTypeDefinition = ( icon: 'visVega', visConfig: { defaults: { spec: getDefaultSpec() } }, editorConfig: { - optionsTemplate: VegaVisEditor, + optionsTemplate: VegaVisEditorComponent, enableAutoApply: true, defaultSize: DefaultEditorSize.MEDIUM, }, - visualization, requestHandler, - responseHandler: 'none', + toExpressionAst, options: { showIndexSelection: false, showQueryBar: true, diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts new file mode 100644 index 0000000000000..54b96813769ba --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.d.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { IServiceSettings } from 'src/plugins/maps_legacy/public'; +import { VegaParser } from '../data_model/vega_parser'; + +interface VegaViewParams { + parentEl: HTMLDivElement; + fireEvent: IInterpreterRenderHandlers['event']; + vegaParser: VegaParser; + serviceSettings: IServiceSettings; + filterManager: DataPublicPluginStart['query']['filterManager']; + timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; + // findIndex: (index: string) => Promise<...>; +} + +export class VegaBaseView { + constructor(params: VegaViewParams); + init(): Promise; + onError(error: any): void; + destroy(): Promise; +} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 979432b2aed2a..25ea77ddbccb4 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -63,7 +63,7 @@ export class VegaBaseView { this._parser = opts.vegaParser; this._serviceSettings = opts.serviceSettings; this._filterManager = opts.filterManager; - this._applyFilter = opts.applyFilter; + this._fireEvent = opts.fireEvent; this._timefilter = opts.timefilter; this._findIndex = opts.findIndex; this._view = null; @@ -264,7 +264,7 @@ export class VegaBaseView { const indexId = await this._findIndex(index); const filter = esFilters.buildQueryFilter(query, indexId); - this._applyFilter({ filters: [filter] }); + this._fireEvent({ name: 'applyFilter', data: { filters: [filter] } }); } /** @@ -301,19 +301,22 @@ export class VegaBaseView { setTimeFilterHandler(start, end) { const { from, to, mode } = VegaBaseView._parseTimeRange(start, end); - this._applyFilter({ - timeFieldName: '*', - filters: [ - { - range: { - '*': { - mode, - gte: from, - lte: to, + this._fireEvent({ + name: 'applyFilter', + data: { + timeFieldName: '*', + filters: [ + { + range: { + '*': { + mode, + gte: from, + lte: to, + }, }, }, - }, - ], + ], + }, }); } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts new file mode 100644 index 0000000000000..a1210e05f4507 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.d.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { VegaBaseView } from './vega_base_view'; + +export class VegaMapView extends VegaBaseView {} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.d.ts b/src/plugins/vis_type_vega/public/vega_view/vega_view.d.ts new file mode 100644 index 0000000000000..c137d8222750c --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.d.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { VegaBaseView } from './vega_base_view'; + +export class VegaView extends VegaBaseView {} diff --git a/src/plugins/vis_type_vega/public/vega_vis_renderer.tsx b/src/plugins/vis_type_vega/public/vega_vis_renderer.tsx new file mode 100644 index 0000000000000..542f59b3dfff9 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_vis_renderer.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { VisualizationContainer } from '../../visualizations/public'; +import { VegaVisualizationDependencies } from './plugin'; +import { RenderValue } from './vega_fn'; +const VegaVisComponent = lazy(() => import('./components/vega_vis_component')); + +export const getVegaVisRenderer: ( + deps: VegaVisualizationDependencies +) => ExpressionRenderDefinition = (deps) => ({ + name: 'vega_vis', + reuseDomNode: true, + render: (domNode, { visData }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + render( + + + , + domNode + ); + }, +}); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index dcf1722768075..837fdf2a9aea3 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -30,8 +30,6 @@ import vegaMapGraph from './test_utils/vega_map_test.json'; import { VegaParser } from './data_model/vega_parser'; import { SearchAPI } from './data_model/search_api'; -import { createVegaTypeDefinition } from './vega_type'; - import { setInjectedVars, setData, setSavedObjects, setNotifications } from './services'; import { coreMock } from '../../../core/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; @@ -49,9 +47,7 @@ jest.mock('./lib/vega', () => ({ describe('VegaVisualizations', () => { let domNode; let VegaVisualization; - let vis; let vegaVisualizationDependencies; - let vegaVisType; let mockWidth; let mockedWidthValue; @@ -91,22 +87,12 @@ describe('VegaVisualizations', () => { getServiceSettings: mockGetServiceSettings, }; - vegaVisType = createVegaTypeDefinition(vegaVisualizationDependencies); VegaVisualization = createVegaVisualization(vegaVisualizationDependencies); }); describe('VegaVisualization - basics', () => { beforeEach(async () => { setupDOM(); - - vis = { - type: vegaVisType, - API: { - events: { - applyFilter: jest.fn(), - }, - }, - }; }); afterEach(() => { @@ -117,7 +103,7 @@ describe('VegaVisualizations', () => { test('should show vegalite graph and update on resize (may fail in dev env)', async () => { let vegaVis; try { - vegaVis = new VegaVisualization(domNode, vis); + vegaVis = new VegaVisualization(domNode, jest.fn()); const vegaParser = new VegaParser( JSON.stringify(vegaliteGraph), @@ -137,7 +123,7 @@ describe('VegaVisualizations', () => { mockedWidthValue = 256; mockedHeightValue = 256; - await vegaVis._vegaView.resize(); + await vegaVis.vegaView.resize(); expect(domNode.innerHTML).toMatchSnapshot(); } finally { @@ -148,7 +134,7 @@ describe('VegaVisualizations', () => { test('should show vega graph (may fail in dev env)', async () => { let vegaVis; try { - vegaVis = new VegaVisualization(domNode, vis); + vegaVis = new VegaVisualization(domNode, jest.fn()); const vegaParser = new VegaParser( JSON.stringify(vegaGraph), new SearchAPI({ @@ -172,7 +158,7 @@ describe('VegaVisualizations', () => { test('should show vega blank rectangle on top of a map (vegamap)', async () => { let vegaVis; try { - vegaVis = new VegaVisualization(domNode, vis); + vegaVis = new VegaVisualization(domNode, jest.fn()); const vegaParser = new VegaParser( JSON.stringify(vegaMapGraph), new SearchAPI({ diff --git a/src/plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.ts similarity index 70% rename from src/plugins/vis_type_vega/public/vega_visualization.js rename to src/plugins/vis_type_vega/public/vega_visualization.ts index 2d58e9cda60cd..58c436bcd4be4 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.ts @@ -17,28 +17,34 @@ * under the License. */ import { i18n } from '@kbn/i18n'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { VegaParser } from './data_model/vega_parser'; +import { VegaVisualizationDependencies } from './plugin'; import { getNotifications, getData, getSavedObjects } from './services'; +import type { VegaView } from './vega_view/vega_view'; -export const createVegaVisualization = ({ getServiceSettings }) => +export const createVegaVisualization = ({ getServiceSettings }: VegaVisualizationDependencies) => class VegaVisualization { - constructor(el, vis) { - this._el = el; - this._vis = vis; + private readonly dataPlugin = getData(); + private readonly savedObjectsClient = getSavedObjects(); + private vegaView: InstanceType | null = null; - this.savedObjectsClient = getSavedObjects(); - this.dataPlugin = getData(); - } + constructor( + private el: HTMLDivElement, + private fireEvent: IInterpreterRenderHandlers['event'] + ) {} /** * Find index pattern by its title, of if not given, gets default * @param {string} [index] * @returns {Promise} index id */ - async findIndex(index) { + async findIndex(index: string) { const { indexPatterns } = this.dataPlugin; let idxObj; if (index) { + // @ts-expect-error idxObj = indexPatterns.findByTitle(this.savedObjectsClient, index); if (!idxObj) { throw new Error( @@ -61,16 +67,10 @@ export const createVegaVisualization = ({ getServiceSettings }) => return idxObj.id; } - /** - * - * @param {VegaParser} visData - * @param {*} status - * @returns {Promise} - */ - async render(visData) { + async render(visData: VegaParser) { const { toasts } = getNotifications(); - if (!visData && !this._vegaView) { + if (!visData && !this.vegaView) { toasts.addWarning( i18n.translate('visTypeVega.visualization.unableToRenderWithoutDataWarningMessage', { defaultMessage: 'Unable to render without data', @@ -82,8 +82,8 @@ export const createVegaVisualization = ({ getServiceSettings }) => try { await this._render(visData); } catch (error) { - if (this._vegaView) { - this._vegaView.onError(error); + if (this.vegaView) { + this.vegaView.onError(error); } else { toasts.addError(error, { title: i18n.translate('visTypeVega.visualization.renderErrorTitle', { @@ -94,20 +94,20 @@ export const createVegaVisualization = ({ getServiceSettings }) => } } - async _render(vegaParser) { + async _render(vegaParser: VegaParser) { if (vegaParser) { // New data received, rebuild the graph - if (this._vegaView) { - await this._vegaView.destroy(); - this._vegaView = null; + if (this.vegaView) { + await this.vegaView.destroy(); + this.vegaView = null; } const serviceSettings = await getServiceSettings(); const { filterManager } = this.dataPlugin.query; const { timefilter } = this.dataPlugin.query.timefilter; const vegaViewParams = { - parentEl: this._el, - applyFilter: this._vis.API.events.applyFilter, + parentEl: this.el, + fireEvent: this.fireEvent, vegaParser, serviceSettings, filterManager, @@ -116,18 +116,17 @@ export const createVegaVisualization = ({ getServiceSettings }) => }; if (vegaParser.useMap) { - const services = { toastService: getNotifications().toasts }; const { VegaMapView } = await import('./vega_view/vega_map_view'); - this._vegaView = new VegaMapView(vegaViewParams, services); + this.vegaView = new VegaMapView(vegaViewParams); } else { - const { VegaView } = await import('./vega_view/vega_view'); - this._vegaView = new VegaView(vegaViewParams); + const { VegaView: VegaViewClass } = await import('./vega_view/vega_view'); + this.vegaView = new VegaViewClass(vegaViewParams); } - await this._vegaView.init(); + await this.vegaView?.init(); } } destroy() { - return this._vegaView && this._vegaView.destroy(); + this.vegaView?.destroy(); } }; diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap index 2cee55e4751c2..b64366c1ce0f3 100644 --- a/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap +++ b/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap @@ -2,12 +2,9 @@ exports[`interpreter/functions#pie returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "vislib_vis", "type": "render", "value": Object { - "params": Object { - "listenOnChange": true, - }, "visConfig": Object { "addLegend": true, "addTooltip": true, diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 0000000000000..c3ffc0dd08412 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`vislib vis toExpressionAst function should match basic snapshot 1`] = ` +Object { + "addArgument": [Function], + "arguments": Object { + "type": Array [ + "area", + ], + "visConfig": Array [ + "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + ], + }, + "getArgument": [Function], + "name": "vislib_vis", + "removeArgument": [Function], + "replaceArgument": [Function], + "toAst": [Function], + "toString": [Function], + "type": "expression_function_builder", +} +`; diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast_pie.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast_pie.test.ts.snap new file mode 100644 index 0000000000000..b8dc4b31747c4 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast_pie.test.ts.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`vislib pie vis toExpressionAst function should match basic snapshot 1`] = ` +Object { + "addArgument": [Function], + "arguments": Object { + "visConfig": Array [ + "{\\"type\\":\\"pie\\",\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"right\\",\\"isDonut\\":true,\\"labels\\":{\\"show\\":true,\\"values\\":true,\\"last_level\\":true,\\"truncate\\":100},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\"},\\"params\\":{}},\\"buckets\\":[{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + ], + }, + "getArgument": [Function], + "name": "vislib_pie_vis", + "removeArgument": [Function], + "replaceArgument": [Function], + "toAst": [Function], + "toString": [Function], + "type": "expression_function_builder", +} +`; diff --git a/src/plugins/vis_type_vislib/public/area.ts b/src/plugins/vis_type_vislib/public/area.ts index ec90fbd1746a1..531958d6b3db3 100644 --- a/src/plugins/vis_type_vislib/public/area.ts +++ b/src/plugins/vis_type_vislib/public/area.ts @@ -37,22 +37,20 @@ import { getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { BasicVislibParams } from './types'; -export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const areaVisTypeDefinition: BaseVisTypeOptions = { name: 'area', title: i18n.translate('visTypeVislib.area.areaTitle', { defaultMessage: 'Area' }), icon: 'visArea', description: i18n.translate('visTypeVislib.area.areaDescription', { defaultMessage: 'Emphasize the quantity beneath a line chart', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + toExpressionAst, visConfig: { defaults: { type: 'area', @@ -131,9 +129,6 @@ export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => labels: {}, }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getConfigCollections(), optionTabs: getAreaOptionTabs(), @@ -190,4 +185,4 @@ export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx index 6109b548f9412..911ee293f580e 100644 --- a/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx @@ -60,4 +60,6 @@ function GaugeOptions(props: VisOptionsProps) { ); } -export { GaugeOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { GaugeOptions as default }; diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx index 7a89496d9441e..312cf60fda6b0 100644 --- a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx @@ -185,4 +185,6 @@ function HeatmapOptions(props: VisOptionsProps) { ); } -export { HeatmapOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { HeatmapOptions as default }; diff --git a/src/plugins/vis_type_vislib/public/components/options/index.tsx b/src/plugins/vis_type_vislib/public/components/options/index.tsx new file mode 100644 index 0000000000000..18c41bf289b11 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/index.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; + +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { ValidationVisOptionsProps } from '../common'; +import { GaugeVisParams } from '../../gauge'; +import { PieVisParams } from '../../pie'; +import { BasicVislibParams } from '../../types'; +import { HeatmapVisParams } from '../../heatmap'; + +const GaugeOptionsLazy = lazy(() => import('./gauge')); +const PieOptionsLazy = lazy(() => import('./pie')); +const PointSeriesOptionsLazy = lazy(() => import('./point_series')); +const HeatmapOptionsLazy = lazy(() => import('./heatmap')); +const MetricsAxisOptionsLazy = lazy(() => import('./metrics_axes')); + +export const GaugeOptions = (props: VisOptionsProps) => ( + +); + +export const PieOptions = (props: VisOptionsProps) => ; + +export const PointSeriesOptions = (props: ValidationVisOptionsProps) => ( + +); + +export const HeatmapOptions = (props: VisOptionsProps) => ( + +); + +export const MetricsAxisOptions = (props: ValidationVisOptionsProps) => ( + +); diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx index 0cc737f19e5c6..63881fea1ad88 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { IAggConfig, IAggType } from 'src/plugins/data/public'; -import { MetricsAxisOptions } from './index'; +import MetricsAxisOptions from './index'; import { BasicVislibParams, SeriesParam, ValueAxis } from '../../../types'; import { ValidationVisOptionsProps } from '../../common'; import { Positions } from '../../../utils/collections'; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx index 18687404b9114..0862c47c35cff 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx @@ -325,4 +325,6 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) ) : null; } -export { MetricsAxisOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { MetricsAxisOptions as default }; diff --git a/src/plugins/vis_type_vislib/public/components/options/pie.tsx b/src/plugins/vis_type_vislib/public/components/options/pie.tsx index 54ba307982967..30828bfc6a3ea 100644 --- a/src/plugins/vis_type_vislib/public/components/options/pie.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/pie.tsx @@ -99,4 +99,6 @@ function PieOptions(props: VisOptionsProps) { ); } -export { PieOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { PieOptions as default }; diff --git a/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts b/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts index fb94ec6743faa..937b92c950430 100644 --- a/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts +++ b/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts @@ -17,4 +17,6 @@ * under the License. */ -export { PointSeriesOptions } from './point_series'; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { PointSeriesOptions as default } from './point_series'; diff --git a/src/plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts index 561c45d26fa7f..86e3b8793d618 100644 --- a/src/plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -24,8 +24,9 @@ import { AggGroupNames } from '../../data/public'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, Alignments, GaugeTypes } from './utils/collections'; import { ColorModes, ColorSchemas, ColorSchemaParams, Labels, Style } from '../../charts/public'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; +import { toExpressionAst } from './to_ast'; +import { BaseVisTypeOptions } from '../../visualizations/public'; +import { BasicVislibParams } from './types'; export interface Gauge extends ColorSchemaParams { backStyle: 'Full'; @@ -55,7 +56,7 @@ export interface GaugeVisParams { gauge: Gauge; } -export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const gaugeVisTypeDefinition: BaseVisTypeOptions = { name: 'gauge', title: i18n.translate('visTypeVislib.gauge.gaugeTitle', { defaultMessage: 'Gauge' }), icon: 'visGauge', @@ -63,6 +64,7 @@ export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => defaultMessage: "Gauges indicate the status of a metric. Use it to show how a metric's value relates to reference threshold values.", }), + toExpressionAst, visConfig: { defaults: { type: 'gauge', @@ -109,7 +111,6 @@ export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => }, }, }, - visualization: createVislibVisController(deps), editorConfig: { collections: getGaugeCollections(), optionsTemplate: GaugeOptions, @@ -145,4 +146,4 @@ export const createGaugeVisTypeDefinition = (deps: VisTypeVislibDependencies) => ]), }, useCustomNoDataScreen: true, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts index 5f74698938a0b..32574fb5b0a9c 100644 --- a/src/plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -21,20 +21,21 @@ import { i18n } from '@kbn/i18n'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, GaugeTypes } from './utils/collections'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { ColorModes, ColorSchemas } from '../../charts/public'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; +import { toExpressionAst } from './to_ast'; +import { BaseVisTypeOptions } from '../../visualizations/public'; +import { BasicVislibParams } from './types'; -export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const goalVisTypeDefinition: BaseVisTypeOptions = { name: 'goal', title: i18n.translate('visTypeVislib.goal.goalTitle', { defaultMessage: 'Goal' }), icon: 'visGoal', description: i18n.translate('visTypeVislib.goal.goalDescription', { defaultMessage: 'A goal chart indicates how close you are to your final goal.', }), - visualization: createVislibVisController(deps), + toExpressionAst, visConfig: { defaults: { addTooltip: true, @@ -110,4 +111,4 @@ export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ]), }, useCustomNoDataScreen: true, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index bd3d02029cb23..f970eddd645f5 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -23,12 +23,11 @@ import { RangeValues, Schemas } from '../../vis_default_editor/public'; import { AggGroupNames } from '../../data/public'; import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections'; import { HeatmapOptions } from './components/options'; -import { createVislibVisController } from './vis_controller'; import { TimeMarker } from './vislib/visualizations/time_marker'; -import { CommonVislibParams, ValueAxis } from './types'; -import { VisTypeVislibDependencies } from './plugin'; +import { BasicVislibParams, CommonVislibParams, ValueAxis } from './types'; import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams { type: 'heatmap'; @@ -42,17 +41,15 @@ export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams times: TimeMarker[]; } -export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const heatmapVisTypeDefinition: BaseVisTypeOptions = { name: 'heatmap', title: i18n.translate('visTypeVislib.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }), icon: 'heatmap', description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix', }), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter]; - }, - visualization: createVislibVisController(deps), + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], + toExpressionAst, visConfig: { defaults: { type: 'heatmap', @@ -86,9 +83,6 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) ], }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getHeatmapCollections(), optionsTemplate: HeatmapOptions, @@ -142,4 +136,4 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts index 8aeeb4ec533ab..d5fb92f5c6a0c 100644 --- a/src/plugins/vis_type_vislib/public/histogram.ts +++ b/src/plugins/vis_type_vislib/public/histogram.ts @@ -36,12 +36,12 @@ import { getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BasicVislibParams } from './types'; +import { toExpressionAst } from './to_ast'; -export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const histogramVisTypeDefinition: BaseVisTypeOptions = { name: 'histogram', title: i18n.translate('visTypeVislib.histogram.histogramTitle', { defaultMessage: 'Vertical Bar', @@ -50,10 +50,8 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies description: i18n.translate('visTypeVislib.histogram.histogramDescription', { defaultMessage: 'Assign a continuous variable to each axis', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + toExpressionAst, visConfig: { defaults: { type: 'histogram', @@ -133,9 +131,6 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies }, }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getConfigCollections(), optionTabs: getAreaOptionTabs(), @@ -192,4 +187,4 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts index 702581828e60d..f1a5365e5ae74 100644 --- a/src/plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts @@ -34,12 +34,12 @@ import { getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BasicVislibParams } from './types'; +import { toExpressionAst } from './to_ast'; -export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const horizontalBarVisTypeDefinition: BaseVisTypeOptions = { name: 'horizontal_bar', title: i18n.translate('visTypeVislib.horizontalBar.horizontalBarTitle', { defaultMessage: 'Horizontal Bar', @@ -48,10 +48,8 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen description: i18n.translate('visTypeVislib.horizontalBar.horizontalBarDescription', { defaultMessage: 'Assign a continuous variable to each axis', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + toExpressionAst, visConfig: { defaults: { type: 'histogram', @@ -130,9 +128,6 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen }, }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getConfigCollections(), optionTabs: getAreaOptionTabs(), @@ -189,4 +184,4 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/index.scss b/src/plugins/vis_type_vislib/public/index.scss index 64445648ba84a..3c347aebde225 100644 --- a/src/plugins/vis_type_vislib/public/index.scss +++ b/src/plugins/vis_type_vislib/public/index.scss @@ -1 +1 @@ -@import './vislib/index' +@import './vislib/index'; diff --git a/src/plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts index 6e9190229114b..a65b0bcf7e2bb 100644 --- a/src/plugins/vis_type_vislib/public/line.ts +++ b/src/plugins/vis_type_vislib/public/line.ts @@ -35,22 +35,20 @@ import { getConfigCollections, } from './utils/collections'; import { getAreaOptionTabs, countLabel } from './utils/common_config'; -import { createVislibVisController } from './vis_controller'; -import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; +import { BasicVislibParams } from './types'; -export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const lineVisTypeDefinition: BaseVisTypeOptions = { name: 'line', title: i18n.translate('visTypeVislib.line.lineTitle', { defaultMessage: 'Line' }), icon: 'visLine', description: i18n.translate('visTypeVislib.line.lineDescription', { defaultMessage: 'Emphasize trends', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush], + toExpressionAst, visConfig: { defaults: { type: 'line', @@ -129,9 +127,6 @@ export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => }, }, }, - events: { - brush: { disabled: false }, - }, editorConfig: { collections: getConfigCollections(), optionTabs: getAreaOptionTabs(), @@ -182,4 +177,4 @@ export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => }, ]), }, -}); +}; diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts index 1e81dbdde3f68..58f7dd0df89e8 100644 --- a/src/plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -23,14 +23,12 @@ import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; import { PieOptions } from './components/options'; import { getPositions, Positions } from './utils/collections'; -import { createVislibVisController } from './vis_controller'; import { CommonVislibParams } from './types'; -import { VisTypeVislibDependencies } from './plugin'; -import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { BaseVisTypeOptions, VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; +import { toExpressionAst } from './to_ast_pie'; export interface PieVisParams extends CommonVislibParams { type: 'pie'; - addLegend: boolean; isDonut: boolean; labels: { show: boolean; @@ -40,17 +38,15 @@ export interface PieVisParams extends CommonVislibParams { }; } -export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ +export const pieVisTypeDefinition: BaseVisTypeOptions = { name: 'pie', title: i18n.translate('visTypeVislib.pie.pieTitle', { defaultMessage: 'Pie' }), icon: 'visPie', description: i18n.translate('visTypeVislib.pie.pieDescription', { defaultMessage: 'Compare parts of a whole', }), - visualization: createVislibVisController(deps), - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter]; - }, + getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter], + toExpressionAst, visConfig: { defaults: { type: 'pie', @@ -108,4 +104,4 @@ export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ( }, hierarchicalData: true, responseHandler: 'vislib_slices', -}); +}; diff --git a/src/plugins/vis_type_vislib/public/pie_fn.ts b/src/plugins/vis_type_vislib/public/pie_fn.ts index bee200cbe30ee..c9da9e9bd9fab 100644 --- a/src/plugins/vis_type_vislib/public/pie_fn.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.ts @@ -18,27 +18,35 @@ */ import { i18n } from '@kbn/i18n'; + import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; + // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; +import { PieVisParams } from './pie'; +import { vislibVisName } from './vis_type_vislib_vis_fn'; + +export const vislibPieName = 'vislib_pie_vis'; interface Arguments { visConfig: string; } -type VisParams = Required; - interface RenderValue { - visConfig: VisParams; + visData: unknown; + visType: string; + visConfig: PieVisParams; } -export const createPieVisFn = (): ExpressionFunctionDefinition< - 'kibana_pie', +export type VisTypeVislibPieExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof vislibPieName, Datatable, Arguments, Render -> => ({ - name: 'kibana_pie', +>; + +export const createPieVisFn = (): VisTypeVislibPieExpressionFunctionDefinition => ({ + name: vislibPieName, type: 'render', inputTypes: ['datatable'], help: i18n.translate('visTypeVislib.functions.pie.help', { @@ -48,23 +56,20 @@ export const createPieVisFn = (): ExpressionFunctionDefinition< visConfig: { types: ['string'], default: '"{}"', - help: '', + help: 'vislib pie vis config', }, }, fn(input, args) { - const visConfig = JSON.parse(args.visConfig); - const convertedData = vislibSlicesResponseHandler(input, visConfig.dimensions); + const visConfig = JSON.parse(args.visConfig) as PieVisParams; + const visData = vislibSlicesResponseHandler(input, visConfig.dimensions); return { type: 'render', - as: 'visualization', + as: vislibVisName, value: { - visData: convertedData, - visType: 'pie', + visData, visConfig, - params: { - listenOnChange: true, - }, + visType: 'pie', }, }; }, diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index c6a6b6f82592b..f183042fd5201 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -17,40 +17,20 @@ * under the License. */ -import './index.scss'; - -import { - CoreSetup, - CoreStart, - Plugin, - IUiSettingsClient, - PluginInitializerContext, -} from 'kibana/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { BaseVisTypeOptions, VisualizationsSetup } from '../../visualizations/public'; import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; import { createPieVisFn } from './pie_fn'; -import { - createHistogramVisTypeDefinition, - createLineVisTypeDefinition, - createPieVisTypeDefinition, - createAreaVisTypeDefinition, - createHeatmapVisTypeDefinition, - createHorizontalBarVisTypeDefinition, - createGaugeVisTypeDefinition, - createGoalVisTypeDefinition, -} from './vis_type_vislib_vis_types'; +import { visLibVisTypeDefinitions, pieVisTypeDefinition } from './vis_type_vislib_vis_types'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; -import { setFormatService, setDataActions, setKibanaLegacy } from './services'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; - -export interface VisTypeVislibDependencies { - uiSettings: IUiSettingsClient; - charts: ChartsPluginSetup; -} +import { setFormatService, setDataActions } from './services'; +import { getVislibVisRenderer } from './vis_renderer'; +import { BasicVislibParams } from './types'; /** @internal */ export interface VisTypeVislibPluginSetupDependencies { @@ -66,54 +46,37 @@ export interface VisTypeVislibPluginStartDependencies { kibanaLegacy: KibanaLegacyStart; } -type VisTypeVislibCoreSetup = CoreSetup; +export type VisTypeVislibCoreSetup = CoreSetup; /** @internal */ -export class VisTypeVislibPlugin implements Plugin { +export class VisTypeVislibPlugin + implements + Plugin { constructor(public initializerContext: PluginInitializerContext) {} public async setup( core: VisTypeVislibCoreSetup, { expressions, visualizations, charts, visTypeXy }: VisTypeVislibPluginSetupDependencies ) { - const visualizationDependencies: Readonly = { - uiSettings: core.uiSettings, - charts, - }; - const vislibTypes = [ - createHistogramVisTypeDefinition, - createLineVisTypeDefinition, - createPieVisTypeDefinition, - createAreaVisTypeDefinition, - createHeatmapVisTypeDefinition, - createHorizontalBarVisTypeDefinition, - createGaugeVisTypeDefinition, - createGoalVisTypeDefinition, - ]; - const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; - // if visTypeXy plugin is disabled it's config will be undefined if (!visTypeXy) { - const convertedTypes: any[] = []; + const convertedTypes: Array> = []; const convertedFns: any[] = []; // Register legacy vislib types that have been converted convertedFns.forEach(expressions.registerFunction); - convertedTypes.forEach((vis) => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); + convertedTypes.forEach(visualizations.createBaseVisualization); + expressions.registerRenderer(getVislibVisRenderer(core, charts)); } - // Register non-converted types - vislibFns.forEach(expressions.registerFunction); - vislibTypes.forEach((vis) => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); + visLibVisTypeDefinitions.forEach(visualizations.createBaseVisualization); + visualizations.createBaseVisualization(pieVisTypeDefinition); + expressions.registerRenderer(getVislibVisRenderer(core, charts)); + [createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction); } - public start(core: CoreStart, { data, kibanaLegacy }: VisTypeVislibPluginStartDependencies) { + public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { setFormatService(data.fieldFormats); setDataActions(data.actions); - setKibanaLegacy(kibanaLegacy); } } diff --git a/src/plugins/vis_type_vislib/public/sample_vis.test.mocks.ts b/src/plugins/vis_type_vislib/public/sample_vis.test.mocks.ts new file mode 100644 index 0000000000000..324e8e00f37fc --- /dev/null +++ b/src/plugins/vis_type_vislib/public/sample_vis.test.mocks.ts @@ -0,0 +1,3307 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const samplePieVis = { + type: { + name: 'pie', + title: 'Pie', + description: 'Compare parts of a whole', + icon: 'visPie', + stage: 'production', + options: { + showTimePicker: true, + showQueryBar: true, + showFilterBar: true, + showIndexSelection: true, + hierarchicalData: false, + }, + visConfig: { + defaults: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + }, + editorConfig: { + collections: { + legendPositions: [ + { + text: 'Top', + value: 'top', + }, + { + text: 'Left', + value: 'left', + }, + { + text: 'Right', + value: 'right', + }, + { + text: 'Bottom', + value: 'bottom', + }, + ], + }, + schemas: { + all: [ + { + group: 'metrics', + name: 'metric', + title: 'Slice size', + min: 1, + max: 1, + aggFilter: ['sum', 'count', 'cardinality', 'top_hits'], + defaults: [ + { + schema: 'metric', + type: 'count', + }, + ], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'segment', + title: 'Split slices', + min: 0, + max: null, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'split', + title: 'Split chart', + mustBeFirst: true, + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + params: [ + { + name: 'row', + default: true, + }, + ], + editor: false, + }, + ], + buckets: [null, null], + metrics: [null], + }, + }, + hidden: false, + requestHandler: 'courier', + responseHandler: 'vislib_slices', + hierarchicalData: true, + useCustomNoDataScreen: false, + }, + title: '[Flights] Airline Carrier', + description: '', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: true, + values: true, + last_level: true, + truncate: 100, + }, + }, + sessionState: {}, + data: { + searchSource: { + id: 'data_source1', + requestStartHandlers: [], + inheritOptions: {}, + history: [], + fields: { + filter: [], + query: { + query: '', + language: 'kuery', + }, + index: { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + title: 'kibana_sample_data_flights', + fieldFormatMap: { + AvgTicketPrice: { + id: 'number', + params: { + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + pattern: '$0,0.[00]', + }, + }, + hour_of_day: { + id: 'number', + params: { + pattern: '00', + }, + }, + }, + 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, + }, + ], + timeFieldName: 'timestamp', + metaFields: ['_source', '_id', '_type', '_index', '_score'], + version: 'WzM1LDFd', + originalSavedObjectBody: { + 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}]', + 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"}}}', + }, + shortDotsEnable: false, + fieldFormats: { + fieldFormats: {}, + defaultMap: { + ip: { + id: 'ip', + params: {}, + }, + date: { + id: 'date', + params: {}, + }, + date_nanos: { + id: 'date_nanos', + params: {}, + es: true, + }, + number: { + id: 'number', + params: {}, + }, + boolean: { + id: 'boolean', + params: {}, + }, + _source: { + id: '_source', + params: {}, + }, + _default_: { + id: 'string', + params: {}, + }, + }, + metaParamsOptions: {}, + }, + }, + }, + dependencies: { + legacy: { + loadingCount$: { + _isScalar: false, + observers: [ + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + destination: { + closed: true, + }, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [ + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 13, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 3, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + null, + ], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + null, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + }, + }, + aggs: { + typesRegistry: {}, + getResponseAggs: () => [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + toSerializedFieldFormat: () => ({ + id: 'number', + }), + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'Carrier', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + toSerializedFieldFormat: () => ({ + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + }, + }), + }, + ], + }, + }, + isHierarchical: () => true, + uiState: { + vis: { + legendOpen: false, + }, + }, +}; + +export const sampleAreaVis = { + type: { + name: 'area', + title: 'Area', + description: 'Emphasize the quantity beneath a line chart', + icon: 'visArea', + stage: 'production', + options: { + showTimePicker: true, + showQueryBar: true, + showFilterBar: true, + showIndexSelection: true, + hierarchicalData: false, + }, + visConfig: { + defaults: { + type: 'area', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'area', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + }, + }, + editorConfig: { + collections: { + legendPositions: [ + { + text: 'Top', + value: 'top', + }, + { + text: 'Left', + value: 'left', + }, + { + text: 'Right', + value: 'right', + }, + { + text: 'Bottom', + value: 'bottom', + }, + ], + positions: [ + { + text: 'Top', + value: 'top', + }, + { + text: 'Left', + value: 'left', + }, + { + text: 'Right', + value: 'right', + }, + { + text: 'Bottom', + value: 'bottom', + }, + ], + chartTypes: [ + { + text: 'Line', + value: 'line', + }, + { + text: 'Area', + value: 'area', + }, + { + text: 'Bar', + value: 'histogram', + }, + ], + axisModes: [ + { + text: 'Normal', + value: 'normal', + }, + { + text: 'Percentage', + value: 'percentage', + }, + { + text: 'Wiggle', + value: 'wiggle', + }, + { + text: 'Silhouette', + value: 'silhouette', + }, + ], + scaleTypes: [ + { + text: 'Linear', + value: 'linear', + }, + { + text: 'Log', + value: 'log', + }, + { + text: 'Square root', + value: 'square root', + }, + ], + chartModes: [ + { + text: 'Normal', + value: 'normal', + }, + { + text: 'Stacked', + value: 'stacked', + }, + ], + interpolationModes: [ + { + text: 'Straight', + value: 'linear', + }, + { + text: 'Smoothed', + value: 'cardinal', + }, + { + text: 'Stepped', + value: 'step-after', + }, + ], + thresholdLineStyles: [ + { + value: 'full', + text: 'Full', + }, + { + value: 'dashed', + text: 'Dashed', + }, + { + value: 'dot-dashed', + text: 'Dot-dashed', + }, + ], + }, + optionTabs: [ + { + name: 'advanced', + title: 'Metrics & axes', + }, + { + name: 'options', + title: 'Panel settings', + }, + ], + schemas: { + all: [ + { + group: 'metrics', + name: 'metric', + title: 'Y-axis', + aggFilter: ['!geo_centroid', '!geo_bounds'], + min: 1, + defaults: [ + { + schema: 'metric', + type: 'count', + }, + ], + max: null, + editor: false, + params: [], + }, + { + group: 'metrics', + name: 'radius', + title: 'Dot size', + min: 0, + max: 1, + aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'segment', + title: 'X-axis', + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'group', + title: 'Split series', + min: 0, + max: 3, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + editor: false, + params: [], + }, + { + group: 'buckets', + name: 'split', + title: 'Split chart', + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + params: [ + { + name: 'row', + default: true, + }, + ], + editor: false, + }, + ], + buckets: [null, null, null], + metrics: [null, null], + }, + }, + hidden: false, + requestHandler: 'courier', + responseHandler: 'none', + hierarchicalData: false, + useCustomNoDataScreen: false, + }, + title: '[eCommerce] Sales by Category', + description: '', + params: { + type: 'area', + grid: { + categoryLines: false, + style: { + color: '#eee', + }, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Sum of total_quantity', + }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { + label: 'Sum of total_quantity', + id: '1', + }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'top', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + dimensions: { + x: { + accessor: 0, + format: { + id: 'date', + params: { + pattern: 'YYYY-MM-DD HH:mm', + }, + }, + params: { + date: true, + interval: 43200000, + format: 'YYYY-MM-DD HH:mm', + bounds: { + min: '2020-09-30T12:41:13.795Z', + max: '2020-10-15T17:00:00.000Z', + }, + }, + label: 'order_date per 12 hours', + aggType: 'date_histogram', + }, + y: [ + { + accessor: 2, + format: { + id: 'number', + params: { + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + }, + }, + params: {}, + label: 'Sum of total_quantity', + aggType: 'sum', + }, + ], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + label: 'category.keyword: Descending', + aggType: 'terms', + }, + ], + }, + }, + sessionState: {}, + data: { + searchSource: { + id: 'data_source1', + requestStartHandlers: [], + inheritOptions: {}, + history: [], + fields: { + query: { + query: '', + language: 'kuery', + }, + filter: [], + index: { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + title: 'kibana_sample_data_ecommerce', + fieldFormatMap: { + taxful_total_price: { + id: 'number', + params: { + pattern: '$0,0.[00]', + }, + }, + }, + fields: [ + { + 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: 'category', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'category.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'category', + }, + }, + }, + { + count: 0, + name: 'currency', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'customer_birth_date', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'customer_first_name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'customer_first_name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'customer_first_name', + }, + }, + }, + { + count: 0, + name: 'customer_full_name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'customer_full_name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'customer_full_name', + }, + }, + }, + { + count: 0, + name: 'customer_gender', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'customer_id', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'customer_last_name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'customer_last_name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'customer_last_name', + }, + }, + }, + { + count: 0, + name: 'customer_phone', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'day_of_week', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'day_of_week_i', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'email', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'event.dataset', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.city_name', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.continent_name', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.country_iso_code', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.location', + type: 'geo_point', + esTypes: ['geo_point'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'geoip.region_name', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'manufacturer', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'manufacturer.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'manufacturer', + }, + }, + }, + { + count: 0, + name: 'order_date', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'order_id', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products._id', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'products._id.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'products._id', + }, + }, + }, + { + count: 0, + name: 'products.base_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.base_unit_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.category', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'products.category.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'products.category', + }, + }, + }, + { + count: 0, + name: 'products.created_on', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.discount_amount', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.discount_percentage', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.manufacturer', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'products.manufacturer.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'products.manufacturer', + }, + }, + }, + { + count: 0, + name: 'products.min_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.product_id', + type: 'number', + esTypes: ['long'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.product_name', + type: 'string', + esTypes: ['text'], + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + }, + { + count: 0, + name: 'products.product_name.keyword', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + subType: { + multi: { + parent: 'products.product_name', + }, + }, + }, + { + count: 0, + name: 'products.quantity', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.sku', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.tax_amount', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.taxful_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.taxless_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'products.unit_discount_amount', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'sku', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'taxful_total_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'taxless_total_price', + type: 'number', + esTypes: ['half_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'total_quantity', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'total_unique_products', + type: 'number', + esTypes: ['integer'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'type', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + count: 0, + name: 'user', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + ], + timeFieldName: 'order_date', + metaFields: ['_source', '_id', '_type', '_index', '_score'], + version: 'WzEzLDFd', + originalSavedObjectBody: { + title: 'kibana_sample_data_ecommerce', + timeFieldName: 'order_date', + fields: + '[{"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":"category","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"category.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"category"}}},{"count":0,"name":"currency","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_birth_date","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_first_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"customer_first_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_first_name"}}},{"count":0,"name":"customer_full_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"customer_full_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_full_name"}}},{"count":0,"name":"customer_gender","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_id","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"customer_last_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"customer_last_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"customer_last_name"}}},{"count":0,"name":"customer_phone","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"day_of_week","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"day_of_week_i","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"email","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"event.dataset","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.city_name","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.continent_name","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.country_iso_code","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.location","type":"geo_point","esTypes":["geo_point"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"geoip.region_name","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"manufacturer","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"manufacturer.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"manufacturer"}}},{"count":0,"name":"order_date","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"order_id","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products._id","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products._id.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products._id"}}},{"count":0,"name":"products.base_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.base_unit_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.category","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products.category.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.category"}}},{"count":0,"name":"products.created_on","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.discount_amount","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.discount_percentage","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.manufacturer","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products.manufacturer.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.manufacturer"}}},{"count":0,"name":"products.min_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.product_id","type":"number","esTypes":["long"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.product_name","type":"string","esTypes":["text"],"scripted":false,"searchable":true,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"products.product_name.keyword","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true,"subType":{"multi":{"parent":"products.product_name"}}},{"count":0,"name":"products.quantity","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.sku","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.tax_amount","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.taxful_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.taxless_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"products.unit_discount_amount","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"sku","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"taxful_total_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"taxless_total_price","type":"number","esTypes":["half_float"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"total_quantity","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"total_unique_products","type":"number","esTypes":["integer"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"type","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"user","type":"string","esTypes":["keyword"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]', + fieldFormatMap: + '{"taxful_total_price":{"id":"number","params":{"parsedUrl":{"origin":"http://localhost:5801","pathname":"/app/visualize","basePath":""},"pattern":"$0,0.[00]"}}}', + }, + shortDotsEnable: false, + fieldFormats: { + fieldFormats: {}, + defaultMap: { + ip: { + id: 'ip', + params: {}, + }, + date: { + id: 'date', + params: {}, + }, + date_nanos: { + id: 'date_nanos', + params: {}, + es: true, + }, + number: { + id: 'number', + params: {}, + }, + boolean: { + id: 'boolean', + params: {}, + }, + _source: { + id: '_source', + params: {}, + }, + _default_: { + id: 'string', + params: {}, + }, + }, + metaParamsOptions: {}, + }, + }, + }, + dependencies: { + legacy: { + loadingCount$: { + _isScalar: false, + observers: [ + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + destination: { + closed: true, + }, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [ + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 13, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 1, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: { + closed: false, + _parentOrParents: null, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + destination: { + closed: false, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + _context: {}, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + count: 3, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: false, + hasPrev: true, + prev: 0, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: true, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [], + active: 1, + index: 2, + }, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + parent: { + closed: true, + _parentOrParents: null, + _subscriptions: null, + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: true, + concurrent: 1, + hasCompleted: true, + buffer: [ + { + _isScalar: false, + }, + ], + active: 1, + index: 1, + }, + }, + _subscriptions: [ + null, + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + subject: { + _isScalar: false, + observers: [null], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + null, + ], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + }, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + null, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + seenValue: false, + }, + _subscriptions: [null], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + }, + _subscriptions: [ + { + closed: false, + _subscriptions: null, + }, + ], + syncErrorValue: null, + syncErrorThrown: false, + syncErrorThrowable: false, + isStopped: false, + hasKey: true, + key: 0, + }, + ], + closed: false, + isStopped: false, + hasError: false, + thrownError: null, + _value: 0, + }, + }, + }, + }, + aggs: { + typesRegistry: {}, + getResponseAggs: () => [ + { + id: '1', + enabled: true, + type: 'sum', + params: { + field: 'total_quantity', + }, + schema: 'metric', + toSerializedFieldFormat: () => ({ + id: 'number', + params: { + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + }, + }), + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'order_date', + timeRange: { + from: '2020-09-30T12:41:13.795Z', + to: '2020-10-15T17:00:00.000Z', + }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + schema: 'segment', + toSerializedFieldFormat: () => ({ + id: 'date', + params: { pattern: 'HH:mm:ss.SSS' }, + }), + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'category.keyword', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + toSerializedFieldFormat: () => ({ + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5801', + pathname: '/app/visualize', + basePath: '', + }, + }, + }), + }, + ], + }, + }, + isHierarchical: () => false, + uiState: {}, +}; diff --git a/src/plugins/vis_type_vislib/public/services.ts b/src/plugins/vis_type_vislib/public/services.ts index 7257b98f2e9f5..633fae9c7f2a6 100644 --- a/src/plugins/vis_type_vislib/public/services.ts +++ b/src/plugins/vis_type_vislib/public/services.ts @@ -19,7 +19,6 @@ import { createGetterSetter } from '../../kibana_utils/public'; import { DataPublicPluginStart } from '../../data/public'; -import { KibanaLegacyStart } from '../../kibana_legacy/public'; export const [getDataActions, setDataActions] = createGetterSetter< DataPublicPluginStart['actions'] @@ -28,7 +27,3 @@ export const [getDataActions, setDataActions] = createGetterSetter< export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('vislib data.fieldFormats'); - -export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter( - 'vislib kibanalegacy' -); diff --git a/src/plugins/vis_type_vislib/public/to_ast.test.ts b/src/plugins/vis_type_vislib/public/to_ast.test.ts new file mode 100644 index 0000000000000..48d3dfe254d0b --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_ast.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from '../../visualizations/public'; +import { buildExpression } from '../../expressions/public'; + +import { BasicVislibParams } from './types'; +import { toExpressionAst } from './to_ast'; +import { sampleAreaVis } from './sample_vis.test.mocks'; + +jest.mock('../../expressions/public', () => ({ + ...(jest.requireActual('../../expressions/public') as any), + buildExpression: jest.fn().mockImplementation(() => ({ + toAst: () => ({ + type: 'expression', + chain: [], + }), + })), +})); + +jest.mock('./to_ast_esaggs', () => ({ + getEsaggsFn: jest.fn(), +})); + +describe('vislib vis toExpressionAst function', () => { + let vis: Vis; + + const params = { + timefilter: {}, + timeRange: {}, + abortSignal: {}, + } as any; + + beforeEach(() => { + vis = sampleAreaVis as any; + }); + + it('should match basic snapshot', () => { + toExpressionAst(vis, params); + const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + + expect(builtExpression).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/to_ast.ts b/src/plugins/vis_type_vislib/public/to_ast.ts new file mode 100644 index 0000000000000..7cd55ccd32ebc --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_ast.ts @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; + +import { VisToExpressionAst, getVisSchemas } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; + +import { vislibVisName, VisTypeVislibExpressionFunctionDefinition } from './vis_type_vislib_vis_fn'; +import { BasicVislibParams } from './types'; +import { + DateHistogramParams, + Dimensions, + HistogramParams, +} from './vislib/helpers/point_series/point_series'; +import { getEsaggsFn } from './to_ast_esaggs'; + +export const toExpressionAst: VisToExpressionAst = async (vis, params) => { + const schemas = getVisSchemas(vis, params); + const dimensions: Dimensions = { + x: schemas.segment ? schemas.segment[0] : null, + y: schemas.metric, + z: schemas.radius, + width: schemas.width, + series: schemas.group, + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }; + + const responseAggs = vis.data.aggs?.getResponseAggs() ?? []; + + if (dimensions.x) { + const xAgg = responseAggs[dimensions.x.accessor] as any; + if (xAgg.type.name === 'date_histogram') { + (dimensions.x.params as DateHistogramParams).date = true; + const { esUnit, esValue } = xAgg.buckets.getInterval(); + (dimensions.x.params as DateHistogramParams).intervalESUnit = esUnit; + (dimensions.x.params as DateHistogramParams).intervalESValue = esValue; + (dimensions.x.params as DateHistogramParams).interval = moment + .duration(esValue, esUnit) + .asMilliseconds(); + (dimensions.x.params as DateHistogramParams).format = xAgg.buckets.getScaledDateFormat(); + (dimensions.x.params as DateHistogramParams).bounds = xAgg.buckets.getBounds(); + } else if (xAgg.type.name === 'histogram') { + const intervalParam = xAgg.type.paramByName('interval'); + const output = { params: {} as any }; + await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, vis.data.searchSource, { + abortSignal: params.abortSignal, + }); + intervalParam.write(xAgg, output); + (dimensions.x.params as HistogramParams).interval = output.params.interval; + } + } + + const visConfig = { ...vis.params }; + + (dimensions.y || []).forEach((yDimension) => { + const yAgg = responseAggs.filter(({ enabled }) => enabled)[yDimension.accessor]; + const seriesParam = (visConfig.seriesParams || []).find((param) => param.data.id === yAgg.id); + if (seriesParam) { + const usedValueAxis = (visConfig.valueAxes || []).find( + (valueAxis) => valueAxis.id === seriesParam.valueAxis + ); + if (usedValueAxis?.scale.mode === 'percentage') { + yDimension.format = { id: 'percent' }; + } + } + if (visConfig?.gauge?.percentageMode === true) { + yDimension.format = { id: 'percent' }; + } + }); + + visConfig.dimensions = dimensions; + + const configStr = JSON.stringify(visConfig).replace(/\\/g, `\\\\`).replace(/'/g, `\\'`); + const visTypeXy = buildExpressionFunction( + vislibVisName, + { + type: vis.type.name, + visConfig: configStr, + } + ); + + const ast = buildExpression([getEsaggsFn(vis), visTypeXy]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_vislib/public/to_ast_esaggs.ts b/src/plugins/vis_type_vislib/public/to_ast_esaggs.ts new file mode 100644 index 0000000000000..a7312c9d36cbb --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_ast_esaggs.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from '../../visualizations/public'; +import { buildExpressionFunction } from '../../expressions/public'; +import { EsaggsExpressionFunctionDefinition } from '../../data/public'; + +import { PieVisParams } from './pie'; +import { BasicVislibParams } from './types'; + +/** + * Get esaggs expressions function + * TODO: replace this with vis.data.aggs!.toExpressionAst(); + * @param vis + */ +export function getEsaggsFn(vis: Vis | Vis) { + return buildExpressionFunction('esaggs', { + index: vis.data.indexPattern!.id!, + metricsAtAllLevels: vis.isHierarchical(), + partialRows: false, + aggConfigs: JSON.stringify(vis.data.aggs!.aggs), + includeFormatHints: false, + }); +} diff --git a/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts b/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts new file mode 100644 index 0000000000000..36a9a17341bc5 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Vis } from '../../visualizations/public'; +import { buildExpression } from '../../expressions/public'; + +import { PieVisParams } from './pie'; +import { samplePieVis } from './sample_vis.test.mocks'; +import { toExpressionAst } from './to_ast_pie'; + +jest.mock('../../expressions/public', () => ({ + ...(jest.requireActual('../../expressions/public') as any), + buildExpression: jest.fn().mockImplementation(() => ({ + toAst: () => ({ + type: 'expression', + chain: [], + }), + })), +})); + +jest.mock('./to_ast_esaggs', () => ({ + getEsaggsFn: jest.fn(), +})); + +describe('vislib pie vis toExpressionAst function', () => { + let vis: Vis; + + const params = { + timefilter: {}, + timeRange: {}, + abortSignal: {}, + } as any; + + beforeEach(() => { + vis = samplePieVis as any; + }); + + it('should match basic snapshot', () => { + toExpressionAst(vis, params); + const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + + expect(builtExpression).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/to_ast_pie.ts b/src/plugins/vis_type_vislib/public/to_ast_pie.ts new file mode 100644 index 0000000000000..95a5f89208ef9 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/to_ast_pie.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getVisSchemas, VisToExpressionAst } from '../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; + +import { PieVisParams } from './pie'; +import { vislibPieName, VisTypeVislibPieExpressionFunctionDefinition } from './pie_fn'; +import { getEsaggsFn } from './to_ast_esaggs'; + +export const toExpressionAst: VisToExpressionAst = async (vis, params) => { + const schemas = getVisSchemas(vis, params); + const visConfig = { + ...vis.params, + dimensions: { + metric: schemas.metric[0], + buckets: schemas.segment, + splitRow: schemas.split_row, + splitColumn: schemas.split_column, + }, + }; + + const configStr = JSON.stringify(visConfig).replace(/\\/g, `\\\\`).replace(/'/g, `\\'`); + const visTypePie = buildExpressionFunction( + vislibPieName, + { + visConfig: configStr, + } + ); + + const ast = buildExpression([getEsaggsFn(vis), visTypePie]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_vislib/public/types.ts b/src/plugins/vis_type_vislib/public/types.ts index a29f0bcb5c52a..c0311edf76154 100644 --- a/src/plugins/vis_type_vislib/public/types.ts +++ b/src/plugins/vis_type_vislib/public/types.ts @@ -29,10 +29,13 @@ import { ThresholdLineStyles, } from './utils/collections'; import { Labels, Style } from '../../charts/public'; +import { Dimensions } from './vislib/helpers/point_series/point_series'; export interface CommonVislibParams { addTooltip: boolean; + addLegend: boolean; legendPosition: Positions; + dimensions: Dimensions; } export interface Scale { @@ -87,6 +90,9 @@ export interface BasicVislibParams extends CommonVislibParams { labels: Labels; thresholdLine: ThresholdLine; valueAxes: ValueAxis[]; + gauge?: { + percentageMode: boolean; + }; grid: { categoryLines: boolean; valueAxis?: string; diff --git a/src/plugins/vis_type_vislib/public/vis_controller.tsx b/src/plugins/vis_type_vislib/public/vis_controller.tsx index 3a05030f804ca..1804d0d52ae7a 100644 --- a/src/plugins/vis_type_vislib/public/vis_controller.tsx +++ b/src/plugins/vis_type_vislib/public/vis_controller.tsx @@ -20,127 +20,147 @@ import $ from 'jquery'; import React, { RefObject } from 'react'; -import { Positions } from './utils/collections'; -import { VisTypeVislibDependencies } from './plugin'; import { mountReactNode } from '../../../core/public/utils'; +import { ChartsPluginSetup } from '../../charts/public'; +import { PersistedState } from '../../visualizations/public'; +import { IInterpreterRenderHandlers } from '../../expressions/public'; + +import { VisTypeVislibCoreSetup } from './plugin'; import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend'; -import { VisParams, ExprVis } from '../../visualizations/public'; -import { getKibanaLegacy } from './services'; +import { BasicVislibParams } from './types'; +import { PieVisParams } from './pie'; const legendClassName = { - top: 'visLib--legend-top', - bottom: 'visLib--legend-bottom', - left: 'visLib--legend-left', - right: 'visLib--legend-right', + top: 'vislib--legend-top', + bottom: 'vislib--legend-bottom', + left: 'vislib--legend-left', + right: 'vislib--legend-right', }; -export const createVislibVisController = (deps: VisTypeVislibDependencies) => { +export type VislibVisController = InstanceType>; + +export const createVislibVisController = ( + core: VisTypeVislibCoreSetup, + charts: ChartsPluginSetup +) => { return class VislibVisController { - unmount: (() => void) | null = null; - visParams?: VisParams; + private removeListeners?: () => void; + private unmountLegend?: () => void; + legendRef: RefObject; container: HTMLDivElement; chartEl: HTMLDivElement; legendEl: HTMLDivElement; - vislibVis: any; + vislibVis?: any; - constructor(public el: Element, public vis: ExprVis) { + constructor(public el: HTMLDivElement) { this.el = el; - this.vis = vis; - this.unmount = null; this.legendRef = React.createRef(); // vis mount point this.container = document.createElement('div'); - this.container.className = 'visLib'; + this.container.className = 'vislib'; this.el.appendChild(this.container); // chart mount point this.chartEl = document.createElement('div'); - this.chartEl.className = 'visLib__chart'; + this.chartEl.className = 'vislib__chart'; this.container.appendChild(this.chartEl); // legend mount point this.legendEl = document.createElement('div'); - this.legendEl.className = 'visLib__legend'; + this.legendEl.className = 'vislib__legend'; this.container.appendChild(this.legendEl); } - render(esResponse: any, visParams: VisParams): Promise { + async render( + esResponse: any, + visParams: BasicVislibParams | PieVisParams, + handlers: IInterpreterRenderHandlers + ): Promise { if (this.vislibVis) { - this.destroy(); + this.destroy(false); + } + + // Used in functional tests to know when chart is loaded by type + this.chartEl.dataset.vislibChartType = visParams.type; + + if (this.el.clientWidth === 0 || this.el.clientHeight === 0) { + handlers.done(); + return; + } + + const [, { kibanaLegacy }] = await core.getStartServices(); + kibanaLegacy.loadFontAwesome(); + + // @ts-expect-error + const { Vis: Vislib } = await import('./vislib/vis'); + const { uiState, event: fireEvent } = handlers; + + this.vislibVis = new Vislib(this.chartEl, visParams, core, charts); + this.vislibVis.on('brush', fireEvent); + this.vislibVis.on('click', fireEvent); + this.vislibVis.on('renderComplete', handlers.done); + this.removeListeners = () => { + this.vislibVis.off('brush', fireEvent); + this.vislibVis.off('click', fireEvent); + }; + + this.vislibVis.initVisConfig(esResponse, uiState); + + if (visParams.addLegend) { + $(this.container) + .attr('class', (i, cls) => { + return cls.replace(/vislib--legend-\S+/g, ''); + }) + .addClass((legendClassName as any)[visParams.legendPosition]); + + this.mountLegend(esResponse, visParams, fireEvent, uiState); } - getKibanaLegacy().loadFontAwesome(); - - return new Promise(async (resolve) => { - if (this.el.clientWidth === 0 || this.el.clientHeight === 0) { - return resolve(); - } - - // @ts-expect-error - const { Vis: Vislib } = await import('./vislib/vis'); - - this.vislibVis = new Vislib(this.chartEl, visParams, deps); - this.vislibVis.on('brush', this.vis.API.events.brush); - this.vislibVis.on('click', this.vis.API.events.filter); - this.vislibVis.on('renderComplete', resolve); - - this.vislibVis.initVisConfig(esResponse, this.vis.getUiState()); - - if (visParams.addLegend) { - $(this.container) - .attr('class', (i, cls) => { - return cls.replace(/visLib--legend-\S+/g, ''); - }) - .addClass((legendClassName as any)[visParams.legendPosition]); - - this.mountLegend(esResponse, visParams.legendPosition); - } - - this.vislibVis.render(esResponse, this.vis.getUiState()); - - // refreshing the legend after the chart is rendered. - // this is necessary because some visualizations - // provide data necessary for the legend only after a render cycle. - if ( - visParams.addLegend && - CUSTOM_LEGEND_VIS_TYPES.includes(this.vislibVis.visConfigArgs.type) - ) { - this.unmountLegend(); - this.mountLegend(esResponse, visParams.legendPosition); - this.vislibVis.render(esResponse, this.vis.getUiState()); - } - }); + this.vislibVis.render(esResponse, uiState); + + // refreshing the legend after the chart is rendered. + // this is necessary because some visualizations + // provide data necessary for the legend only after a render cycle. + if ( + visParams.addLegend && + CUSTOM_LEGEND_VIS_TYPES.includes(this.vislibVis.visConfigArgs.type) + ) { + this.unmountLegend?.(); + this.mountLegend(esResponse, visParams, fireEvent, uiState); + this.vislibVis.render(esResponse, uiState); + } } - mountLegend(visData: any, position: Positions) { - this.unmount = mountReactNode( + mountLegend( + visData: unknown, + { legendPosition, addLegend }: BasicVislibParams | PieVisParams, + fireEvent: IInterpreterRenderHandlers['event'], + uiState?: PersistedState + ) { + this.unmountLegend = mountReactNode( )(this.legendEl); } - unmountLegend() { - if (this.unmount) { - this.unmount(); - } - } + destroy(clearElement = true) { + this.unmountLegend?.(); - destroy() { - if (this.unmount) { - this.unmount(); + if (clearElement) { + this.el.innerHTML = ''; } if (this.vislibVis) { - this.vislibVis.off('brush', this.vis.API.events.brush); - this.vislibVis.off('click', this.vis.API.events.filter); + this.removeListeners?.(); this.vislibVis.destroy(); delete this.vislibVis; } diff --git a/src/plugins/vis_type_vislib/public/vis_renderer.tsx b/src/plugins/vis_type_vislib/public/vis_renderer.tsx new file mode 100644 index 0000000000000..9c697f481e63e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vis_renderer.tsx @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { ExpressionRenderDefinition } from '../../expressions/public'; +import { VisualizationContainer } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +import { VisTypeVislibCoreSetup } from './plugin'; +import { VislibRenderValue, vislibVisName } from './vis_type_vislib_vis_fn'; + +function shouldShowNoResultsMessage(visData: any, visType: string): boolean { + if (['goal', 'gauge'].includes(visType)) { + return false; + } + + const rows: object[] | undefined = visData?.rows; + const isZeroHits = visData?.hits === 0 || (rows && !rows.length); + + return Boolean(isZeroHits); +} + +const VislibWrapper = lazy(() => import('./vis_wrapper')); + +export const getVislibVisRenderer: ( + core: VisTypeVislibCoreSetup, + charts: ChartsPluginSetup +) => ExpressionRenderDefinition = (core, charts) => ({ + name: vislibVisName, + reuseDomNode: true, + render: async (domNode, config, handlers) => { + const showNoResult = shouldShowNoResultsMessage(config.visData, config.visType); + + handlers.onDestroy(() => unmountComponentAtNode(domNode)); + + render( + + + , + domNode + ); + }, +}); diff --git a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts index 557f9930f55b1..c5fa8f36f43e3 100644 --- a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts @@ -18,29 +18,35 @@ */ import { i18n } from '@kbn/i18n'; + import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; + // @ts-ignore import { vislibSeriesResponseHandler } from './vislib/response_handler'; +import { BasicVislibParams } from './types'; + +export const vislibVisName = 'vislib_vis'; interface Arguments { type: string; visConfig: string; } -type VisParams = Required; - -interface RenderValue { +export interface VislibRenderValue { + visData: any; visType: string; - visConfig: VisParams; + visConfig: BasicVislibParams; } -export const createVisTypeVislibVisFn = (): ExpressionFunctionDefinition< - 'vislib', +export type VisTypeVislibExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof vislibVisName, Datatable, Arguments, - Render -> => ({ - name: 'vislib', + Render +>; + +export const createVisTypeVislibVisFn = (): VisTypeVislibExpressionFunctionDefinition => ({ + name: vislibVisName, type: 'render', inputTypes: ['datatable'], help: i18n.translate('visTypeVislib.functions.vislib.help', { @@ -55,23 +61,21 @@ export const createVisTypeVislibVisFn = (): ExpressionFunctionDefinition< visConfig: { types: ['string'], default: '"{}"', - help: '', + help: 'vislib vis config', }, }, fn(context, args) { - const visConfigParams = JSON.parse(args.visConfig); - const convertedData = vislibSeriesResponseHandler(context, visConfigParams.dimensions); + const visType = args.type; + const visConfig = JSON.parse(args.visConfig) as BasicVislibParams; + const visData = vislibSeriesResponseHandler(context, visConfig.dimensions); return { type: 'render', - as: 'visualization', + as: vislibVisName, value: { - visData: convertedData, - visType: args.type, - visConfig: visConfigParams, - params: { - listenOnChange: true, - }, + visData, + visConfig, + visType, }, }; }, diff --git a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts index f44d503895483..1b43a213c618d 100644 --- a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts @@ -17,11 +17,22 @@ * under the License. */ -export { createHistogramVisTypeDefinition } from './histogram'; -export { createLineVisTypeDefinition } from './line'; -export { createPieVisTypeDefinition } from './pie'; -export { createAreaVisTypeDefinition } from './area'; -export { createHeatmapVisTypeDefinition } from './heatmap'; -export { createHorizontalBarVisTypeDefinition } from './horizontal_bar'; -export { createGaugeVisTypeDefinition } from './gauge'; -export { createGoalVisTypeDefinition } from './goal'; +import { histogramVisTypeDefinition } from './histogram'; +import { lineVisTypeDefinition } from './line'; +import { areaVisTypeDefinition } from './area'; +import { heatmapVisTypeDefinition } from './heatmap'; +import { horizontalBarVisTypeDefinition } from './horizontal_bar'; +import { gaugeVisTypeDefinition } from './gauge'; +import { goalVisTypeDefinition } from './goal'; + +export { pieVisTypeDefinition } from './pie'; + +export const visLibVisTypeDefinitions = [ + histogramVisTypeDefinition, + lineVisTypeDefinition, + areaVisTypeDefinition, + heatmapVisTypeDefinition, + horizontalBarVisTypeDefinition, + gaugeVisTypeDefinition, + goalVisTypeDefinition, +]; diff --git a/src/plugins/vis_type_vislib/public/vis_wrapper.tsx b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx new file mode 100644 index 0000000000000..980ba1c175885 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vis_wrapper.tsx @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useMemo, useRef } from 'react'; +import { EuiResizeObserver } from '@elastic/eui'; +import { debounce } from 'lodash'; + +import { IInterpreterRenderHandlers } from '../../expressions/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +import { VislibRenderValue } from './vis_type_vislib_vis_fn'; +import { createVislibVisController, VislibVisController } from './vis_controller'; +import { VisTypeVislibCoreSetup } from './plugin'; + +import './index.scss'; + +type VislibWrapperProps = VislibRenderValue & { + core: VisTypeVislibCoreSetup; + charts: ChartsPluginSetup; + handlers: IInterpreterRenderHandlers; +}; + +const VislibWrapper = ({ core, charts, visData, visConfig, handlers }: VislibWrapperProps) => { + const chartDiv = useRef(null); + const visController = useRef(null); + + const updateChart = useMemo( + () => + debounce(() => { + if (visController.current) { + visController.current.render(visData, visConfig, handlers); + } + }, 100), + [visConfig, visData, handlers] + ); + + useEffect(() => { + if (chartDiv.current) { + const Controller = createVislibVisController(core, charts); + visController.current = new Controller(chartDiv.current); + } + return () => { + visController.current?.destroy(); + visController.current = null; + }; + }, [core, charts, handlers]); + + useEffect(updateChart, [updateChart]); + + useEffect(() => { + if (handlers.uiState) { + handlers.uiState.on('change', updateChart); + + return () => { + handlers.uiState?.off('change', updateChart); + }; + } + }, [handlers.uiState, updateChart]); + + return ( + + {(resizeRef) => ( +
+
+
+ )} + + ); +}; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VislibWrapper as default }; diff --git a/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss b/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss index c03aa19140de0..843bb9d3f03eb 100644 --- a/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss +++ b/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss @@ -1,27 +1,29 @@ -.visLib { +.vislib { flex: 1 1 0; display: flex; flex-direction: row; overflow: auto; - &.visLib--legend-left { + &.vislib--legend-left { flex-direction: row-reverse; } - &.visLib--legend-right { + &.vislib--legend-right { flex-direction: row; } - &.visLib--legend-top { + &.vislib--legend-top { flex-direction: column-reverse; } - &.visLib--legend-bottom { + &.vislib--legend-bottom { flex-direction: column; } } -.visLib__chart { +.vislib__chart, +.vislib__wrapper, +.vislib__container { display: flex; flex: 1 1 auto; min-height: 0; diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss b/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss index b1a59f88a348a..a06f0cb00787b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss @@ -13,6 +13,7 @@ $visLegendLineHeight: $euiSize; left: 0; display: flex; padding: $euiSizeXS; + margin: $euiSizeS; background-color: $euiColorEmptyShade; transition: opacity $euiAnimSpeedFast $euiAnimSlightResistance, background-color $euiAnimSpeedFast $euiAnimSlightResistance $euiAnimSpeedExtraSlow; @@ -33,13 +34,13 @@ $visLegendLineHeight: $euiSize; height: 100%; } -.visLib--legend-left { +.vislib--legend-left { .visLegend__list { margin-bottom: $euiSizeL; } } -.visLib--legend-bottom { +.vislib--legend-bottom { .visLegend__list { margin-left: $euiSizeL; } @@ -64,8 +65,8 @@ $visLegendLineHeight: $euiSize; } } - .visLib--legend-top &, - .visLib--legend-bottom & { + .vislib--legend-top &, + .vislib--legend-bottom & { width: auto; flex-direction: row; flex-wrap: wrap; diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 65d148cfc5ef4..a3fb536d0aec5 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -41,16 +41,8 @@ jest.mock('../../../services', () => ({ }), })); -const vis = { - params: { - addLegend: true, - }, - API: { - events: { - filter: jest.fn(), - }, - }, -}; +const fireEvent = jest.fn(); + const vislibVis = { handler: { highlight: jest.fn(), @@ -96,14 +88,15 @@ const uiState = { set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)), emit: jest.fn(), setSilent: jest.fn(), -}; +} as any; const getWrapper = async (props?: Partial) => { const wrapper = mount( { }); it('should work with no handlers set', () => { - const newVis = { - ...vis, + const newProps = { vislibVis: { ...vislibVis, handler: null, @@ -197,7 +189,7 @@ describe('VisLegend Component', () => { }; expect(async () => { - wrapper = await getWrapper({ vis: newVis }); + wrapper = await getWrapper(newProps); const first = getLegendItems(wrapper).first(); first.simulate('focus'); first.simulate('blur'); @@ -216,8 +208,11 @@ describe('VisLegend Component', () => { const filterGroup = wrapper.find(EuiButtonGroup).first(); filterGroup.getElement().props.onChange('filterIn'); - expect(vis.API.events.filter).toHaveBeenCalledWith({ data: ['valuesA'], negate: false }); - expect(vis.API.events.filter).toHaveBeenCalledTimes(1); + expect(fireEvent).toHaveBeenCalledWith({ + name: 'filterBucket', + data: { data: ['valuesA'], negate: false }, + }); + expect(fireEvent).toHaveBeenCalledTimes(1); }); it('should filter in when clicked', () => { @@ -226,8 +221,11 @@ describe('VisLegend Component', () => { const filterGroup = wrapper.find(EuiButtonGroup).first(); filterGroup.getElement().props.onChange('filterOut'); - expect(vis.API.events.filter).toHaveBeenCalledWith({ data: ['valuesA'], negate: true }); - expect(vis.API.events.filter).toHaveBeenCalledTimes(1); + expect(fireEvent).toHaveBeenCalledWith({ + name: 'filterBucket', + data: { data: ['valuesA'], negate: true }, + }); + expect(fireEvent).toHaveBeenCalledTimes(1); }); }); diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index 5a2db2d21c6fe..cec97f0cadf11 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -23,16 +23,21 @@ import { compact, uniqBy, map, every, isUndefined } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiPopoverProps, EuiIcon, keys, htmlIdGenerator } from '@elastic/eui'; +import { PersistedState } from '../../../../../visualizations/public'; +import { IInterpreterRenderHandlers } from '../../../../../expressions/public'; + import { getDataActions } from '../../../services'; import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; import { VisLegendItem } from './legend_item'; import { getPieNames } from './pie_utils'; +import { BasicVislibParams } from '../../../types'; export interface VisLegendProps { - vis: any; vislibVis: any; - visData: any; - uiState: any; + visData: unknown; + uiState?: PersistedState; + fireEvent: IInterpreterRenderHandlers['event']; + addLegend: BasicVislibParams['addLegend']; position: 'top' | 'bottom' | 'left' | 'right'; } @@ -49,7 +54,10 @@ export class VisLegend extends PureComponent { constructor(props: VisLegendProps) { super(props); - const open = props.uiState.get('vis.legendOpen', true); + + // TODO: Check when this bwc can safely be removed + const bwcLegendStateDefault = props.addLegend ?? true; + const open = props.uiState?.get('vis.legendOpen', bwcLegendStateDefault) as boolean; this.state = { open, @@ -64,13 +72,9 @@ export class VisLegend extends PureComponent { } toggleLegend = () => { - const bwcAddLegend = this.props.vis.params.addLegend; - const bwcLegendStateDefault = bwcAddLegend == null ? true : bwcAddLegend; - const newOpen = !this.props.uiState.get('vis.legendOpen', bwcLegendStateDefault); - this.setState({ open: newOpen }); - // open should be applied on template before we update uiState - setTimeout(() => { - this.props.uiState.set('vis.legendOpen', newOpen); + const newOpen = !this.state.open; + this.setState({ open: newOpen }, () => { + this.props.uiState?.set('vis.legendOpen', newOpen); }); }; @@ -79,17 +83,23 @@ export class VisLegend extends PureComponent { return; } - const colors = this.props.uiState.get('vis.colors') || {}; + const colors = this.props.uiState?.get('vis.colors') || {}; if (colors[label] === color) delete colors[label]; else colors[label] = color; - this.props.uiState.setSilent('vis.colors', null); - this.props.uiState.set('vis.colors', colors); - this.props.uiState.emit('colorChanged'); + this.props.uiState?.setSilent('vis.colors', null); + this.props.uiState?.set('vis.colors', colors); + this.props.uiState?.emit('colorChanged'); this.refresh(); }; filter = ({ values: data }: LegendItem, negate: boolean) => { - this.props.vis.API.events.filter({ data, negate }); + this.props.fireEvent({ + name: 'filterBucket', + data: { + data, + negate, + }, + }); }; canFilter = async (item: LegendItem): Promise => { @@ -172,11 +182,8 @@ export class VisLegend extends PureComponent { return; } // make sure vislib is defined at this point - if ( - this.props.uiState.get('vis.legendOpen') == null && - this.props.vis.params.addLegend != null - ) { - this.setState({ open: this.props.vis.params.addLegend }); + if (this.props.uiState?.get('vis.legendOpen') == null && this.props.addLegend != null) { + this.setState({ open: this.props.addLegend }); } if (vislibVis.visConfig) { diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts index 32536960c59cd..147bae7e3b985 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts @@ -33,11 +33,11 @@ export function initXAxis(chart: Chart, table: Table) { chart.xAxisLabel = title; if ('interval' in params) { - const { interval } = params; if ('date' in params) { const { intervalESUnit, intervalESValue } = params; + chart.ordered = { - interval: moment.duration(interval), + interval: moment.duration(intervalESValue, intervalESUnit as any), intervalESUnit, intervalESValue, }; diff --git a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts index 4ba3101d34898..f40d01e6a8123 100644 --- a/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts @@ -28,7 +28,7 @@ import { Column, Table } from '../../types'; export interface DateHistogramParams { date: boolean; - interval: string; + interval: number | string; intervalESValue: number; intervalESUnit: string; format: string; @@ -57,6 +57,9 @@ export interface Dimensions { y: Dimension[]; z?: Dimension[]; series?: Dimension | Dimension[]; + width?: Dimension[]; + splitRow?: Dimension[]; + splitColumn?: Dimension[]; } export interface Aspect { accessor: Column['id']; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss deleted file mode 100644 index d1c7fc7c6278c..0000000000000 --- a/src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss +++ /dev/null @@ -1,17 +0,0 @@ -.visError { - flex: 1 1 0; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - - // From ML - .top { align-self: flex-start; } - .bottom { align-self: flex-end; } -} - -// Prevent large request errors from overflowing the container -.visError--request { - max-width: 100%; - max-height: 100%; -} diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss index b19c2dfb153b9..6751e9f28a8ee 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss +++ b/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss @@ -1,4 +1,3 @@ @import './alerts'; -@import './handler'; @import './layout/index'; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js index 4c50472b9d11a..16283c6bddf26 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js @@ -182,7 +182,6 @@ export class Dispatch { const data = d.input || d; return { - e: d3.event, data: isSlices ? this._pieClickResponse(data) : this._seriesClickResponse(data), }; } @@ -423,7 +422,6 @@ export class Dispatch { */ createBrush(xScale, svg) { const self = this; - const visConfig = self.handler.visConfig; const { width, height } = svg.node().getBBox(); const isHorizontal = self.handler.categoryAxes[0].axisConfig.isHorizontal(); @@ -449,8 +447,6 @@ export class Dispatch { return self.emit('brush', { range, - config: visConfig, - e: d3.event, data, }); }); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js index 3c1aeaa0d1d0d..938ea3adcb9b5 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js @@ -46,10 +46,10 @@ const markdownIt = new MarkdownIt({ * create the visualization */ export class Handler { - constructor(vis, visConfig, deps) { + constructor(vis, visConfig, uiSettings) { this.el = visConfig.get('el'); this.ChartClass = chartTypes[visConfig.get('type')]; - this.deps = deps; + this.uiSettings = uiSettings; this.charts = []; this.vis = vis; @@ -91,12 +91,18 @@ export class Handler { const xRaw = _.get(eventPayload.data, 'series[0].values[0].xRaw'); if (!xRaw) return; // not sure if this is possible? return self.vis.emit(eventType, { - table: xRaw.table, - range: eventPayload.range, - column: xRaw.column, + name: 'brush', + data: { + table: xRaw.table, + range: eventPayload.range, + column: xRaw.column, + }, }); case 'click': - return self.vis.emit(eventType, eventPayload); + return self.vis.emit(eventType, { + name: 'filterBucket', + data: eventPayload, + }); } }; }); @@ -164,7 +170,7 @@ export class Handler { let loadedCount = 0; const chartSelection = selection.selectAll('.chart'); chartSelection.each(function (chartData) { - const chart = new self.ChartClass(self, this, chartData, self.deps); + const chart = new self.ChartClass(self, this, chartData, self.uiSettings); self.vis.eventNames().forEach(function (event) { self.enable(event, chart); @@ -222,7 +228,7 @@ export class Handler { // class name needs `chart` in it for the polling checkSize function // to continuously call render on resize .attr('class', 'visError chart error') - .attr('data-test-subj', 'visLibVisualizeError'); + .attr('data-test-subj', 'vislibVisualizeError'); div.append('h4').text(markdownIt.renderInline(message)); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js index d50c70de1bb48..119a24d2f25d1 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js @@ -157,7 +157,7 @@ dateHistogramArray.forEach(function (data, i) { const args = Array.from(arguments); expect(args.length).toBe(2); expect(args[0]).toBe('click'); - expect(args[1]).toBe(event); + expect(args[1].data).toBe(event); done(); }; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss b/src/plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss index 96c72bd5956d2..b3fdf654f8ab7 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss @@ -32,7 +32,7 @@ min-height: 0; min-width: 0; overflow: hidden; - padding: ($euiSizeS + 2px) 0; + padding: $euiSizeS 0; } .visWrapper__column { @@ -142,7 +142,6 @@ min-height: 0; } - // // STYLE // @@ -153,7 +152,6 @@ // could it be easily found to apply to all chart types. // At least wrapping selectors inside .visWrapper will narrow scope. - // sass-lint:disable-block no-mergeable-selectors // Keep SVG and non-renamable selectors separately .visWrapper { @@ -221,7 +219,7 @@ .axis-title text { @include fontSize($euiFontSizeXS); font-weight: $euiFontWeightBold; - fill: $visTextColor; + fill: $euiTextColor; } .y-axis-title { diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js index dda9d85ec43c5..2086f744be584 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js @@ -50,7 +50,6 @@ export class VisConfig { return _.get(this._values, property, defaults); } else { throw new Error(`Accessing invalid config property: ${property}`); - return defaults; } } diff --git a/src/plugins/vis_type_vislib/public/vislib/vis.js b/src/plugins/vis_type_vislib/public/vislib/vis.js index f258cb55ba281..628b876fc50c5 100644 --- a/src/plugins/vis_type_vislib/public/vislib/vis.js +++ b/src/plugins/vis_type_vislib/public/vislib/vis.js @@ -35,13 +35,14 @@ import { DIMMING_OPACITY_SETTING, HEATMAP_MAX_BUCKETS_SETTING } from '../../comm * @param config {Object} Parameters that define the chart type and chart options */ export class Vis extends EventEmitter { - constructor(element, visConfigArgs, deps) { + constructor(element, visConfigArgs, core, charts) { super(); this.element = element.get ? element.get(0) : element; this.visConfigArgs = _.cloneDeep(visConfigArgs); - this.visConfigArgs.dimmingOpacity = deps.uiSettings.get(DIMMING_OPACITY_SETTING); - this.visConfigArgs.heatmapMaxBuckets = deps.uiSettings.get(HEATMAP_MAX_BUCKETS_SETTING); - this.deps = deps; + this.visConfigArgs.dimmingOpacity = core.uiSettings.get(DIMMING_OPACITY_SETTING); + this.visConfigArgs.heatmapMaxBuckets = core.uiSettings.get(HEATMAP_MAX_BUCKETS_SETTING); + this.charts = charts; + this.uiSettings = core.uiSettings; } hasLegend() { @@ -56,7 +57,7 @@ export class Vis extends EventEmitter { this.data, this.uiState, this.element, - this.deps.charts.colors.createColorLookupFunction.bind(this.deps.charts.colors) + this.charts.colors.createColorLookupFunction.bind(this.charts.colors) ); } @@ -78,7 +79,7 @@ export class Vis extends EventEmitter { this.initVisConfig(data, uiState); - this.handler = new Handler(this, this.visConfig, this.deps); + this.handler = new Handler(this, this.visConfig, this.uiSettings); this._runOnHandler('render'); } diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js index 5ed6d3eb79f4b..e5cb0235b6510 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js @@ -39,13 +39,13 @@ import { * @param chartData {Object} Elasticsearch query results for this specific chart */ export class Chart { - constructor(handler, element, chartData, deps) { + constructor(handler, element, chartData, uiSettings) { this.handler = handler; this.chartEl = element; this.chartData = chartData; this.tooltips = []; - const events = (this.events = new Dispatch(handler, deps.uiSettings)); + const events = (this.events = new Dispatch(handler, uiSettings)); const fieldFormatter = getFormatService().deserialize( this.handler.data.get('tooltipFormatter') diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js index 0ffa53fc7ca9c..11b5964eebdf3 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js @@ -53,20 +53,10 @@ afterEach(function () { count = 0; }); -const getDeps = () => { - const mockUiSettings = coreMock.createSetup().uiSettings; - const charts = chartPluginMock.createStartContract(); - - return { - uiSettings: mockUiSettings, - charts: charts, - }; -}; - -export function getVis(visLibParams, element) { +export function getVis(vislibParams, element) { return new Vis( element || $visCanvas.new(), - _.defaults({}, visLibParams || {}, { + _.defaults({}, vislibParams || {}, { addTooltip: true, addLegend: true, defaultYExtents: false, @@ -74,6 +64,7 @@ export function getVis(visLibParams, element) { yAxis: {}, type: 'histogram', }), - getDeps() + coreMock.createSetup(), + chartPluginMock.createStartContract() ); } diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js index 6fdc2a134b820..913b237a10e58 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js @@ -28,7 +28,7 @@ import { getVis } from './_vis_fixture'; describe('Vislib Gauge Chart Test Suite', function () { let vis; let chartEl; - const visLibParams = { + const vislibParams = { type: 'gauge', addTooltip: true, addLegend: false, @@ -71,7 +71,7 @@ describe('Vislib Gauge Chart Test Suite', function () { }; function generateVis(opts = {}) { - const config = _.defaultsDeep({}, opts, visLibParams); + const config = _.defaultsDeep({}, opts, vislibParams); if (vis) { vis.destroy(); $('.visChart').remove(); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js index 938d3d0ec6d74..b1acea34aabf6 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js @@ -42,8 +42,8 @@ const defaults = { * @param chartData {Object} Elasticsearch query results for this specific chart */ export class PieChart extends Chart { - constructor(handler, chartEl, chartData, deps) { - super(handler, chartEl, chartData, deps); + constructor(handler, chartEl, chartData, uiSettings) { + super(handler, chartEl, chartData, uiSettings); const charts = this.handler.data.getVisData(); this._validatePieData(charts); this._attr = _.defaults(handler.visConfig.get('chart', {}), defaults); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js index e2da33d0808ba..1207db2e54bb8 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js @@ -40,7 +40,7 @@ let mockedSVGElementGetBBox; let mockedSVGElementGetComputedTextLength; describe('No global chart settings', function () { - const visLibParams1 = { + const vislibParams1 = { el: '
', type: 'pie', addLegend: true, @@ -58,7 +58,7 @@ describe('No global chart settings', function () { }); beforeEach(() => { - chart1 = getVis(visLibParams1); + chart1 = getVis(vislibParams1); mockUiState = getMockUiState(); }); @@ -153,7 +153,7 @@ describe('Vislib PieChart Class Test Suite', function () { describe('Vislib PieChart Class Test Suite for ' + names[i] + ' data', function () { const mockPieData = pieChartMockData[aggItem]; - const visLibParams = { + const vislibParams = { type: 'pie', addLegend: true, addTooltip: true, @@ -161,7 +161,7 @@ describe('Vislib PieChart Class Test Suite', function () { let vis; beforeEach(async () => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); const mockUiState = getMockUiState(); vis.render(mockPieData, mockUiState); }); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js index 9a25d041f6567..a40d46737f05e 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js @@ -40,10 +40,10 @@ const touchdownTmpl = _.template(touchdownTmplHtml); * @param chartData {Object} Elasticsearch query results for this specific chart */ export class PointSeries extends Chart { - constructor(handler, chartEl, chartData, deps) { - super(handler, chartEl, chartData, deps); + constructor(handler, chartEl, chartData, uiSettings) { + super(handler, chartEl, chartData, uiSettings); - this.deps = deps; + this.uiSettings = uiSettings; this.handler = handler; this.chartData = chartData; this.chartEl = chartEl; @@ -246,7 +246,13 @@ export class PointSeries extends Chart { if (!seriArgs.show) return; const SeriClass = seriTypes[seriArgs.type || self.handler.visConfig.get('chart.type')] || seriTypes.line; - const series = new SeriClass(self.handler, svg, data.series[i], seriArgs, self.deps); + const series = new SeriClass( + self.handler, + svg, + data.series[i], + seriArgs, + self.uiSettings + ); series.events = self.events; svg.call(series.draw()); self.series.push(series); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js index e3e2d31ecd4f4..b65c5be330fef 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js @@ -43,8 +43,8 @@ const defaults = { * chart */ export class AreaChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { - super(handler, chartEl, chartData, seriesConfigArgs, deps); + constructor(handler, chartEl, chartData, seriesConfigArgs, core) { + super(handler, chartEl, chartData, seriesConfigArgs, core); this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); this.isOverlapping = this.seriesConfig.mode !== 'stacked'; diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js index 3cd58060978ee..ae15d95d560ca 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js @@ -38,7 +38,7 @@ const dataTypesArray = { stackedSeries: import('../../../fixtures/mock_data/date_histogram/_stacked_series'), }; -const visLibParams = { +const vislibParams = { type: 'area', addLegend: true, addTooltip: true, @@ -61,7 +61,7 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) { }); beforeEach(async () => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); vis.render(await dataType, mockUiState); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js index 1369bf1dff68a..07bebf4eb2f83 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js @@ -57,8 +57,8 @@ function datumWidth(defaultWidth, datum, nextDatum, scale, gutterWidth, groupCou * @param chartData {Object} Elasticsearch query results for this specific chart */ export class ColumnChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { - super(handler, chartEl, chartData, seriesConfigArgs, deps); + constructor(handler, chartEl, chartData, seriesConfigArgs, core) { + super(handler, chartEl, chartData, seriesConfigArgs, core); this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); this.labelOptions = _.defaults(handler.visConfig.get('labels', {}), defaults.showLabel); } diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js index f3d8d66df2d85..d7fc177a30009 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js @@ -62,7 +62,7 @@ dataTypesArray.forEach(function (dataType) { describe('Vislib Column Chart Test Suite for ' + name + ' Data', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'histogram', addLegend: true, addTooltip: true, @@ -81,7 +81,7 @@ dataTypesArray.forEach(function (dataType) { }); beforeEach(() => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); vis.render(data, mockUiState); @@ -261,7 +261,7 @@ dataTypesArray.forEach(function (dataType) { describe('stackData method - data set with zeros in percentage mode', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'histogram', addLegend: true, addTooltip: true, @@ -276,7 +276,7 @@ describe('stackData method - data set with zeros in percentage mode', function ( }); beforeEach(() => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); }); @@ -320,7 +320,7 @@ describe('stackData method - data set with zeros in percentage mode', function ( describe('datumWidth - split chart data set with holes', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'histogram', addLegend: true, addTooltip: true, @@ -335,7 +335,7 @@ describe('datumWidth - split chart data set with holes', function () { }); beforeEach(() => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); vis.render(rowsSeriesWithHoles, mockUiState); @@ -366,7 +366,7 @@ describe('datumWidth - split chart data set with holes', function () { describe('datumWidth - monthly interval', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'histogram', addLegend: true, addTooltip: true, @@ -384,7 +384,7 @@ describe('datumWidth - monthly interval', function () { }); beforeEach(() => { - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.on('brush', _.noop); vis.render(seriesMonthlyInterval, mockUiState); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js index 8c727d225c6c3..a7534add76e36 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js @@ -71,7 +71,7 @@ describe('Vislib Heatmap Chart Test Suite', function () { describe('for ' + name + ' Data', function () { let vis; let mockUiState; - const visLibParams = { + const vislibParams = { type: 'heatmap', addLegend: true, addTooltip: true, @@ -84,7 +84,7 @@ describe('Vislib Heatmap Chart Test Suite', function () { }; function generateVis(opts = {}) { - const config = _.defaultsDeep({}, opts, visLibParams); + const config = _.defaultsDeep({}, opts, vislibParams); vis = getVis(config); mockUiState = getMockUiState(); vis.on('brush', _.noop); diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js index 64fbae7d1ac8c..983c15c004b97 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js @@ -42,8 +42,8 @@ const defaults = { * @param chartData {Object} Elasticsearch query results for this specific chart */ export class LineChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { - super(handler, chartEl, chartData, seriesConfigArgs, deps); + constructor(handler, chartEl, chartData, seriesConfigArgs, core) { + super(handler, chartEl, chartData, seriesConfigArgs, core); this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); } diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js index a84c74c095051..8d86df7c27da4 100644 --- a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js @@ -71,14 +71,14 @@ describe('Vislib Line Chart', function () { let mockUiState; beforeEach(() => { - const visLibParams = { + const vislibParams = { type: 'line', addLegend: true, addTooltip: true, drawLinesBetweenPoints: true, }; - vis = getVis(visLibParams); + vis = getVis(vislibParams); mockUiState = getMockUiState(); vis.render(data, mockUiState); vis.on('brush', _.noop); diff --git a/src/plugins/visualizations/public/components/_visualization.scss b/src/plugins/visualizations/public/components/_visualization.scss index f5e2d4fcf2862..bde9621fd70b8 100644 --- a/src/plugins/visualizations/public/components/_visualization.scss +++ b/src/plugins/visualizations/public/components/_visualization.scss @@ -70,10 +70,10 @@ flex-direction: column; } -.visChart__spinner { +.visChart__spinner, .visError { display: flex; flex: 1 1 auto; justify-content: center; align-items: center; + text-align: center; } - diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index a810b4b65528f..c12a0f0759018 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -374,6 +374,7 @@ export class VisualizeEmbeddable query: this.input.query, filters: this.input.filters, }, + searchSessionId: this.input.searchSessionId, uiState: this.vis.uiState, inspectorAdapters: this.inspectorAdapters, }; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 081399fd1fbea..7bd4466b23166 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -52,6 +52,7 @@ export { ISavedVis, VisSavedObject, VisResponseValue, + VisToExpressionAst, } from './types'; export { ExprVisAPIEvents } from './expressions/vis'; export { VisualizationListItem } from './vis_types/vis_type_alias_registry'; diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index cbdecd4aac747..2c6cfc6fb7462 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -6,12 +6,8 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metrics/tsvb function 1`] = `"tsvb params='{\\"foo\\":\\"bar\\"}' uiState='{}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles pie function 1`] = `"kibana_pie visConfig='{\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"buckets\\":[1,2]}}' "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"bucket\\":1}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index c744043ed155b..0c210a04d2007 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -21,14 +21,12 @@ import { prepareJson, prepareString, buildPipelineVisFunction, - buildVislibDimensions, buildPipeline, SchemaConfig, Schemas, } from './build_pipeline'; import { Vis } from '..'; import { dataPluginMock } from '../../../../plugins/data/public/mocks'; -import { IndexPattern, IAggConfigs } from '../../../../plugins/data/public'; import { parseExpression } from '../../../expressions/common'; describe('visualize loader pipeline helpers: build pipeline', () => { @@ -94,14 +92,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { uiState = {}; }); - it('handles vega function', () => { - const vis = { - params: { spec: 'this is a test' }, - }; - const actual = buildPipelineVisFunction.vega(vis.params, schemasDef, uiState); - expect(actual).toMatchSnapshot(); - }); - it('handles input_control_vis function', () => { const params = { some: 'nested', @@ -144,15 +134,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { const actual = buildPipelineVisFunction.tile_map(params, schemas, uiState); expect(actual).toMatchSnapshot(); }); - - it('handles pie function', () => { - const schemas = { - ...schemasDef, - segment: [1, 2], - }; - const actual = buildPipelineVisFunction.pie({}, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); }); describe('buildPipeline', () => { @@ -182,157 +163,4 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(expression).toMatchSnapshot(); }); }); - - describe('buildVislibDimensions', () => { - const dataStart = dataPluginMock.createStartContract(); - - let aggs: IAggConfigs; - let vis: Vis; - let params: any; - - beforeEach(() => { - aggs = dataStart.search.aggs.createAggConfigs({} as IndexPattern, [ - { - id: '0', - enabled: true, - type: 'count', - schema: 'metric', - params: {}, - }, - ]); - - params = { - searchSource: null, - timefilter: dataStart.query.timefilter.timefilter, - timeRange: null, - }; - }); - - describe('test y dimension format for histogram chart', () => { - beforeEach(() => { - vis = { - // @ts-ignore - type: { - name: 'histogram', - }, - params: { - seriesParams: [ - { - data: { id: '0' }, - valueAxis: 'axis-y', - }, - ], - valueAxes: [ - { - id: 'axis-y', - scale: { - mode: 'normal', - }, - }, - ], - }, - data: { - aggs, - searchSource: {} as any, - }, - isHierarchical: () => { - return false; - }, - }; - }); - - it('with one numeric metric in regular moder', async () => { - const dimensions = await buildVislibDimensions(vis, params); - const expected = { id: 'number' }; - const actual = dimensions.y[0].format; - expect(actual).toEqual(expected); - }); - - it('with one numeric metric in percentage mode', async () => { - vis.params.valueAxes[0].scale.mode = 'percentage'; - const dimensions = await buildVislibDimensions(vis, params); - const expected = { id: 'percent' }; - const actual = dimensions.y[0].format; - expect(actual).toEqual(expected); - }); - - it('with two numeric metrics, mixed normal and percent mode should have corresponding formatters', async () => { - aggs.createAggConfig({ - id: '5', - enabled: true, - type: 'count', - schema: 'metric', - params: {}, - }); - - vis.params = { - seriesParams: [ - { - data: { id: '0' }, - valueAxis: 'axis-y-1', - }, - { - data: { id: '5' }, - valueAxis: 'axis-y-2', - }, - ], - valueAxes: [ - { - id: 'axis-y-1', - scale: { - mode: 'normal', - }, - }, - { - id: 'axis-y-2', - scale: { - mode: 'percentage', - }, - }, - ], - }; - - const dimensions = await buildVislibDimensions(vis, params); - const expectedY1 = { id: 'number' }; - const expectedY2 = { id: 'percent' }; - expect(dimensions.y[0].format).toEqual(expectedY1); - expect(dimensions.y[1].format).toEqual(expectedY2); - }); - }); - - describe('test y dimension format for gauge chart', () => { - beforeEach(() => { - vis = { - // @ts-ignore - type: { - name: 'gauge', - }, - params: { gauge: {} }, - data: { - aggs, - searchSource: {} as any, - }, - isHierarchical: () => { - return false; - }, - }; - }); - - it('with percentageMode = false', async () => { - vis.params.gauge.percentageMode = false; - const dimensions = await buildVislibDimensions(vis, params); - const expected = { id: 'number' }; - const actual = dimensions.y[0].format; - expect(actual).toEqual(expected); - }); - - it('with percentageMode = true', async () => { - vis.params.gauge.percentageMode = true; - const dimensions = await buildVislibDimensions(vis, params); - const expected = { id: 'percent' }; - const actual = dimensions.y[0].format; - expect(actual).toEqual(expected); - }); - }); - }); }); diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index eb431212166a3..3593d62b9d2e6 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -17,8 +17,6 @@ * under the License. */ -import { get } from 'lodash'; -import moment from 'moment'; import { formatExpression, SerializedFieldFormat } from '../../../../plugins/expressions/public'; import { IAggConfig, search, TimefilterContract } from '../../../../plugins/data/public'; import { Vis, VisParams } from '../types'; @@ -76,16 +74,6 @@ export interface BuildPipelineParams { abortSignal?: AbortSignal; } -const vislibCharts: string[] = [ - 'area', - 'gauge', - 'goal', - 'heatmap', - 'histogram', - 'horizontal_bar', - 'line', -]; - export const getSchemas = ( vis: Vis, { timeRange, timefilter }: BuildPipelineParams @@ -230,33 +218,7 @@ export const prepareDimension = (variable: string, data: any) => { return expr; }; -const adjustVislibDimensionFormmaters = (vis: Vis, dimensions: { y: any[] }): void => { - const visConfig = vis.params; - const responseAggs = vis.data.aggs!.getResponseAggs().filter((agg: IAggConfig) => agg.enabled); - - (dimensions.y || []).forEach((yDimension) => { - const yAgg = responseAggs[yDimension.accessor]; - const seriesParam = (visConfig.seriesParams || []).find( - (param: any) => param.data.id === yAgg.id - ); - if (seriesParam) { - const usedValueAxis = (visConfig.valueAxes || []).find( - (valueAxis: any) => valueAxis.id === seriesParam.valueAxis - ); - if (get(usedValueAxis, 'scale.mode') === 'percentage') { - yDimension.format = { id: 'percent' }; - } - } - if (get(visConfig, 'gauge.percentageMode') === true) { - yDimension.format = { id: 'percent' }; - } - }); -}; - export const buildPipelineVisFunction: BuildPipelineVisFunction = { - vega: (params) => { - return `vega ${prepareString('spec', params.spec)}`; - }, input_control_vis: (params) => { return `input_control_vis ${prepareJson('visConfig', params)}`; }, @@ -281,13 +243,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { }; return `tilemap ${prepareJson('visConfig', visConfig)}`; }, - pie: (params, schemas) => { - const visConfig = { - ...params, - ...buildVisConfig.pie(schemas), - }; - return `kibana_pie ${prepareJson('visConfig', visConfig)}`; - }, }; const buildVisConfig: BuildVisConfigFunction = { @@ -308,55 +263,6 @@ const buildVisConfig: BuildVisConfigFunction = { }; return visConfig; }, - pie: (schemas) => { - const visConfig = {} as any; - visConfig.dimensions = { - metric: schemas.metric[0], - buckets: schemas.segment, - splitRow: schemas.split_row, - splitColumn: schemas.split_column, - }; - return visConfig; - }, -}; - -export const buildVislibDimensions = async (vis: any, params: BuildPipelineParams) => { - const schemas = getSchemas(vis, { - timeRange: params.timeRange, - timefilter: params.timefilter, - }); - const dimensions = { - x: schemas.segment ? schemas.segment[0] : null, - y: schemas.metric, - z: schemas.radius, - width: schemas.width, - series: schemas.group, - splitRow: schemas.split_row, - splitColumn: schemas.split_column, - }; - if (schemas.segment) { - const xAgg = vis.data.aggs.getResponseAggs()[dimensions.x.accessor]; - if (xAgg.type.name === 'date_histogram') { - dimensions.x.params.date = true; - const { esUnit, esValue } = xAgg.buckets.getInterval(); - dimensions.x.params.interval = moment.duration(esValue, esUnit); - dimensions.x.params.intervalESValue = esValue; - dimensions.x.params.intervalESUnit = esUnit; - dimensions.x.params.format = xAgg.buckets.getScaledDateFormat(); - dimensions.x.params.bounds = xAgg.buckets.getBounds(); - } else if (xAgg.type.name === 'histogram') { - const intervalParam = xAgg.type.paramByName('interval'); - const output = { params: {} as any }; - await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, vis.data.searchSource, { - abortSignal: params.abortSignal, - }); - intervalParam.write(xAgg, output); - dimensions.x.params.interval = output.params.interval; - } - } - - adjustVislibDimensionFormmaters(vis, dimensions); - return dimensions; }; export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { @@ -399,11 +305,6 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { schemas, uiState ); - } else if (vislibCharts.includes(vis.type.name)) { - const visConfig = { ...vis.params }; - visConfig.dimensions = await buildVislibDimensions(vis, params); - - pipeline += `vislib type='${vis.type.name}' ${prepareJson('visConfig', visConfig)}`; } else { const visConfig = { ...vis.params }; visConfig.dimensions = schemas; diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index 68ab3561d375c..3c322f5e79165 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -76,4 +76,4 @@ export interface VisToExpressionAstParams { export type VisToExpressionAst = ( vis: Vis, params: VisToExpressionAstParams -) => ExpressionAstExpression; +) => Promise | ExpressionAstExpression; diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.ts similarity index 88% rename from test/api_integration/apis/saved_objects/export.js rename to test/api_integration/apis/saved_objects/export.ts index 0c37e6b782a35..7254f3b3fcf31 100644 --- a/test/api_integration/apis/saved_objects/export.js +++ b/test/api_integration/apis/saved_objects/export.ts @@ -18,8 +18,12 @@ */ import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService }) { +function ndjsonToObject(input: string) { + return input.split('\n').map((str) => JSON.parse(str)); +} +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); @@ -38,7 +42,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = ndjsonToObject(resp.text); expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -61,7 +65,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = ndjsonToObject(resp.text); expect(objects).to.have.length(3); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -86,7 +90,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = ndjsonToObject(resp.text); expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -109,7 +113,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = resp.text.split('\n').map((str) => JSON.parse(str)); expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -133,7 +137,7 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - const objects = resp.text.split('\n').map(JSON.parse); + const objects = ndjsonToObject(resp.text); expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); @@ -217,6 +221,51 @@ export default function ({ getService }) { }); }); }); + + it('should export object with circular refs', async () => { + const soWithCycliRefs = [ + { + type: 'dashboard', + id: 'dashboard-a', + attributes: { + title: 'dashboard-a', + }, + references: [ + { + name: 'circular-dashboard-ref', + id: 'dashboard-b', + type: 'dashboard', + }, + ], + }, + { + type: 'dashboard', + id: 'dashboard-b', + attributes: { + title: 'dashboard-b', + }, + references: [ + { + name: 'circular-dashboard-ref', + id: 'dashboard-a', + type: 'dashboard', + }, + ], + }, + ]; + await supertest.post('/api/saved_objects/_bulk_create').send(soWithCycliRefs).expect(200); + const resp = await supertest + .post('/api/saved_objects/_export') + .send({ + includeReferencesDeep: true, + type: ['dashboard'], + }) + .expect(200); + + const objects = ndjsonToObject(resp.text); + expect(objects.find((o) => o.id === 'dashboard-a')).to.be.ok(); + expect(objects.find((o) => o.id === 'dashboard-b')).to.be.ok(); + }); }); describe('10,000 objects', () => { @@ -245,11 +294,11 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - expect(resp.headers['content-disposition']).to.eql( + expect(resp.header['content-disposition']).to.eql( 'attachment; filename="export.ndjson"' ); - expect(resp.headers['content-type']).to.eql('application/ndjson'); - const objects = resp.text.split('\n').map(JSON.parse); + expect(resp.header['content-type']).to.eql('application/ndjson'); + const objects = ndjsonToObject(resp.text); expect(objects).to.eql([ { attributes: { @@ -304,11 +353,11 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - expect(resp.headers['content-disposition']).to.eql( + expect(resp.header['content-disposition']).to.eql( 'attachment; filename="export.ndjson"' ); - expect(resp.headers['content-type']).to.eql('application/ndjson'); - const objects = resp.text.split('\n').map(JSON.parse); + expect(resp.header['content-type']).to.eql('application/ndjson'); + const objects = ndjsonToObject(resp.text); expect(objects).to.eql([ { attributes: { @@ -368,11 +417,11 @@ export default function ({ getService }) { }) .expect(200) .then((resp) => { - expect(resp.headers['content-disposition']).to.eql( + expect(resp.header['content-disposition']).to.eql( 'attachment; filename="export.ndjson"' ); - expect(resp.headers['content-type']).to.eql('application/ndjson'); - const objects = resp.text.split('\n').map(JSON.parse); + expect(resp.header['content-type']).to.eql('application/ndjson'); + const objects = ndjsonToObject(resp.text); expect(objects).to.eql([ { attributes: { @@ -443,7 +492,7 @@ export default function ({ getService }) { }); describe('10,001 objects', () => { - let customVisId; + let customVisId: string; before(async () => { await esArchiver.load('saved_objects/10k'); await supertest diff --git a/test/api_integration/apis/saved_objects/import.js b/test/api_integration/apis/saved_objects/import.ts similarity index 78% rename from test/api_integration/apis/saved_objects/import.js rename to test/api_integration/apis/saved_objects/import.ts index 1666df2c83e5a..bdb695ef20dd1 100644 --- a/test/api_integration/apis/saved_objects/import.js +++ b/test/api_integration/apis/saved_objects/import.ts @@ -19,8 +19,19 @@ import expect from '@kbn/expect'; import { join } from 'path'; +import dedent from 'dedent'; +import type { SavedObjectsImportError } from 'src/core/server'; +import type { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService }) { +const createConflictError = ( + object: Omit +): SavedObjectsImportError => ({ + ...object, + title: object.meta.title, + error: { type: 'conflict' }, +}); + +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); @@ -41,11 +52,6 @@ export default function ({ getService }) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', meta: { title: 'Requests', icon: 'dashboardApp' }, }; - const createError = (object, type) => ({ - ...object, - title: object.meta.title, - error: { type }, - }); describe('with kibana index', () => { describe('with basic data existing', () => { @@ -75,9 +81,9 @@ export default function ({ getService }) { success: false, successCount: 0, errors: [ - createError(indexPattern, 'conflict'), - createError(visualization, 'conflict'), - createError(dashboard, 'conflict'), + createConflictError(indexPattern), + createConflictError(visualization), + createConflictError(dashboard), ], }); }); @@ -128,6 +134,43 @@ export default function ({ getService }) { }); }); + it('should return 200 when importing SO with circular refs', async () => { + const fileBuffer = Buffer.from( + dedent` + {"attributes":{"title":"dashboard-b"},"id":"dashboard-b","references":[{"id":"dashboard-a","name":"circular-dashboard-ref","type":"dashboard"}],"type":"dashboard"} + {"attributes":{"title":"dashboard-a"},"id":"dashboard-a","references":[{"id":"dashboard-b","name":"circular-dashboard-ref","type":"dashboard"}],"type":"dashboard"} + `, + 'utf8' + ); + const resp = await supertest + .post('/api/saved_objects/_import') + .attach('file', fileBuffer, 'export.ndjson') + .expect(200); + + expect(resp.body).to.eql({ + success: true, + successCount: 2, + successResults: [ + { + id: 'dashboard-b', + meta: { + icon: 'dashboardApp', + title: 'dashboard-b', + }, + type: 'dashboard', + }, + { + id: 'dashboard-a', + meta: { + icon: 'dashboardApp', + title: 'dashboard-a', + }, + type: 'dashboard', + }, + ], + }); + }); + it('should return 400 when trying to import more than 10,000 objects', async () => { const fileChunks = []; for (let i = 0; i < 10001; i++) { diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 7fc120f9ea474..d3c0fe834958d 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -88,7 +88,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await footer.getVisibleText()).to.have.string(rowsHardLimit); }); - describe('expand a document row', function () { + // FLAKY: https://github.com/elastic/kibana/issues/81632 + describe.skip('expand a document row', function () { const rowToInspect = 1; beforeEach(async function () { // close the toggle if open diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.ts similarity index 89% rename from test/functional/apps/management/_import_objects.js rename to test/functional/apps/management/_import_objects.ts index 3941b117e6efe..ddabb32bf5909 100644 --- a/test/functional/apps/management/_import_objects.js +++ b/test/functional/apps/management/_import_objects.ts @@ -20,10 +20,14 @@ import expect from '@kbn/expect'; import path from 'path'; import { keyBy } from 'lodash'; +import { FtrProviderContext } from '../../ftr_provider_context'; -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +function uniq(input: T[]): T[] { + return [...new Set(input)]; +} -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); @@ -67,6 +71,24 @@ export default function ({ getService, getPageObjects }) { expect(flyout['Log Agents'].relationship).to.eql('Parent'); }); + it('should import saved objects with circular refs', async function () { + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_import_objects_circular_refs.ndjson') + ); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + + await PageObjects.savedObjects.clickRelationshipsByTitle('dashboard-a'); + + const flyoutContent = await PageObjects.savedObjects.getRelationshipFlyout(); + + expect(uniq(flyoutContent.map(({ relationship }) => relationship).sort())).to.eql([ + 'Child', + 'Parent', + ]); + expect(uniq(flyoutContent.map(({ title }) => title))).to.eql(['dashboard-b']); + }); + it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () { await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_conflicts.ndjson') @@ -399,21 +421,40 @@ export default function ({ getService, getPageObjects }) { expect(isSavedObjectImported).to.be(true); }); - it('should import saved objects with index patterns when index patterns does not exists', async () => { - // First, we need to delete the index pattern - await PageObjects.savedObjects.clickCheckboxByTitle('logstash-*'); - await PageObjects.savedObjects.clickDelete(); - - // Then, import the objects + it('should preserve index patterns selection when switching between pages', async () => { await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json') + path.join(__dirname, 'exports', '_import_objects_missing_all_index_patterns.json') ); - await PageObjects.savedObjects.checkImportSucceeded(); - await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.savedObjects.getRowTitles(); - const isSavedObjectImported = objects.includes('saved object imported with index pattern'); - expect(isSavedObjectImported).to.be(true); + await PageObjects.savedObjects.setOverriddenIndexPatternValue( + 'missing-index-pattern-1', + 'index-pattern-test-1' + ); + + await testSubjects.click('pagination-button-next'); + + await PageObjects.savedObjects.setOverriddenIndexPatternValue( + 'missing-index-pattern-7', + 'index-pattern-test-2' + ); + + await testSubjects.click('pagination-button-previous'); + + const selectedIdForMissingIndexPattern1 = await testSubjects.getAttribute( + 'managementChangeIndexSelection-missing-index-pattern-1', + 'value' + ); + + expect(selectedIdForMissingIndexPattern1).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a20'); + + await testSubjects.click('pagination-button-next'); + + const selectedIdForMissingIndexPattern7 = await testSubjects.getAttribute( + 'managementChangeIndexSelection-missing-index-pattern-7', + 'value' + ); + + expect(selectedIdForMissingIndexPattern7).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a87'); }); }); }); diff --git a/test/functional/apps/management/exports/_import_objects_circular_refs.ndjson b/test/functional/apps/management/exports/_import_objects_circular_refs.ndjson new file mode 100644 index 0000000000000..44297e9e8f3e0 --- /dev/null +++ b/test/functional/apps/management/exports/_import_objects_circular_refs.ndjson @@ -0,0 +1,2 @@ +{"attributes":{"title":"dashboard-b"},"id":"dashboard-b","references":[{"id":"dashboard-a","name":"circular-dashboard-ref","type":"dashboard"}],"type":"dashboard"} +{"attributes":{"title":"dashboard-a"},"id":"dashboard-a","references":[{"id":"dashboard-b","name":"circular-dashboard-ref","type":"dashboard"}],"type":"dashboard"} diff --git a/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json b/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json new file mode 100644 index 0000000000000..45572b0bf34fe --- /dev/null +++ b/test/functional/apps/management/exports/_import_objects_missing_all_index_patterns.json @@ -0,0 +1,121 @@ +[ + { + "_id": "test-vis-1", + "_type": "visualization", + "_source": { + "title": "Test VIS 1", + "visState": "{\"title\":\"test vis 1\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-1\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-2", + "_type": "visualization", + "_source": { + "title": "Test VIS 2", + "visState": "{\"title\":\"test vis 2\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-2\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-3", + "_type": "visualization", + "_source": { + "title": "Test VIS 3", + "visState": "{\"title\":\"test vis 3\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-3\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-4", + "_type": "visualization", + "_source": { + "title": "Test VIS 4", + "visState": "{\"title\":\"test vis 4\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-4\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-5", + "_type": "visualization", + "_source": { + "title": "Test VIS 5", + "visState": "{\"title\":\"test vis 5\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-5\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-6", + "_type": "visualization", + "_source": { + "title": "Test VIS 6", + "visState": "{\"title\":\"test vis 6\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-6\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + }, + { + "_id": "test-vis-7", + "_type": "visualization", + "_source": { + "title": "Test VIS 7", + "visState": "{\"title\":\"test vis 7\",\"type\":\"histogram\"}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"missing-index-pattern-7\",\"query\":{}}" + } + }, + "_meta": { + "savedObjectVersion": 2 + } + } +] diff --git a/test/functional/apps/management/index.js b/test/functional/apps/management/index.ts similarity index 94% rename from test/functional/apps/management/index.js rename to test/functional/apps/management/index.ts index d5f0c286af7a5..7365f912ea4fa 100644 --- a/test/functional/apps/management/index.js +++ b/test/functional/apps/management/index.ts @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, loadTestFile }) { +export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('management', function () { diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index 8e65b6488836c..0742f14b2eeb4 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -126,6 +126,12 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv } } + async setOverriddenIndexPatternValue(oldName: string, newName: string) { + const select = await testSubjects.find(`managementChangeIndexSelection-${oldName}`); + const option = await testSubjects.findDescendant(`indexPatternOption-${newName}`, select); + await option.click(); + } + async clickCopyToSpaceByTitle(title: string) { const table = keyBy(await this.getElementsInTable(), 'title'); // should we check if table size > 0 and log error if not? diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 3ac6c83e61f14..272fc5a7914be 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -35,12 +35,18 @@ export type CommonlyUsed = export function TimePickerProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); - const retry = getService('retry'); const find = getService('find'); const browser = getService('browser'); const testSubjects = getService('testSubjects'); const { header } = getPageObjects(['header']); const kibanaServer = getService('kibanaServer'); + const MenuToggle = getService('MenuToggle'); + + const quickSelectTimeMenuToggle = new MenuToggle({ + name: 'QuickSelectTime Menu', + menuTestSubject: 'superDatePickerQuickMenu', + toggleButtonTestSubject: 'superDatePickerToggleQuickMenuButton', + }); class TimePicker { defaultStartTime = 'Sep 19, 2015 @ 06:31:44.000'; @@ -158,34 +164,8 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo return await find.existsByCssSelector('.euiDatePickerRange--readOnly'); } - public async isQuickSelectMenuOpen() { - return await testSubjects.exists('superDatePickerQuickMenu'); - } - - public async openQuickSelectTimeMenu() { - log.debug('openQuickSelectTimeMenu'); - const isMenuOpen = await this.isQuickSelectMenuOpen(); - if (!isMenuOpen) { - log.debug('opening quick select menu'); - await retry.try(async () => { - await testSubjects.click('superDatePickerToggleQuickMenuButton'); - }); - } - } - - public async closeQuickSelectTimeMenu() { - log.debug('closeQuickSelectTimeMenu'); - const isMenuOpen = await this.isQuickSelectMenuOpen(); - if (isMenuOpen) { - log.debug('closing quick select menu'); - await retry.try(async () => { - await testSubjects.click('superDatePickerToggleQuickMenuButton'); - }); - } - } - public async getRefreshConfig(keepQuickSelectOpen = false) { - await this.openQuickSelectTimeMenu(); + await quickSelectTimeMenuToggle.open(); const interval = await testSubjects.getAttribute( 'superDatePickerRefreshIntervalInput', 'value' @@ -207,7 +187,7 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo 'superDatePickerToggleRefreshButton' ); if (!keepQuickSelectOpen) { - await this.closeQuickSelectTimeMenu(); + await quickSelectTimeMenuToggle.close(); } return { @@ -270,26 +250,26 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo } public async startAutoRefresh(intervalS = 3) { - await this.openQuickSelectTimeMenu(); + await quickSelectTimeMenuToggle.open(); await this.inputValue('superDatePickerRefreshIntervalInput', intervalS.toString()); const refreshConfig = await this.getRefreshConfig(true); if (refreshConfig.isPaused) { log.debug('start auto refresh'); await testSubjects.click('superDatePickerToggleRefreshButton'); } - await this.closeQuickSelectTimeMenu(); + await quickSelectTimeMenuToggle.close(); } public async pauseAutoRefresh() { log.debug('pauseAutoRefresh'); const refreshConfig = await this.getRefreshConfig(true); + if (!refreshConfig.isPaused) { log.debug('pause auto refresh'); await testSubjects.click('superDatePickerToggleRefreshButton'); - await this.closeQuickSelectTimeMenu(); } - await this.closeQuickSelectTimeMenu(); + await quickSelectTimeMenuToggle.close(); } public async resumeAutoRefresh() { @@ -300,7 +280,7 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo await testSubjects.click('superDatePickerToggleRefreshButton'); } - await this.closeQuickSelectTimeMenu(); + await quickSelectTimeMenuToggle.close(); } public async setHistoricalDataRange() { diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index cb114866322db..1acea624ad4cd 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -218,7 +218,7 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr } public async expectError() { - await testSubjects.existOrFail('visLibVisualizeError'); + await testSubjects.existOrFail('vislibVisualizeError'); } public async getVisualizationRenderingCount() { diff --git a/test/functional/screenshots/baseline/tsvb_dashboard.png b/test/functional/screenshots/baseline/tsvb_dashboard.png index fe63e352aef78..b0264ce8d4592 100644 Binary files a/test/functional/screenshots/baseline/tsvb_dashboard.png and b/test/functional/screenshots/baseline/tsvb_dashboard.png differ diff --git a/test/functional/services/common/test_subjects.ts b/test/functional/services/common/test_subjects.ts index 204edcb215107..e464eaf943627 100644 --- a/test/functional/services/common/test_subjects.ts +++ b/test/functional/services/common/test_subjects.ts @@ -170,11 +170,27 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { public async getAttribute( selector: string, attribute: string, - timeout?: number + options?: + | number + | { + findTimeout?: number; + tryTimeout?: number; + } ): Promise { - return await retry.try(async () => { - log.debug(`TestSubjects.getAttribute(${selector}, ${attribute})`); - const element = await this.find(selector, timeout); + const findTimeout = + (typeof options === 'number' ? options : options?.findTimeout) ?? + config.get('timeouts.find'); + + const tryTimeout = + (typeof options !== 'number' ? options?.tryTimeout : undefined) ?? + config.get('timeouts.try'); + + log.debug( + `TestSubjects.getAttribute(${selector}, ${attribute}, tryTimeout=${tryTimeout}, findTimeout=${findTimeout})` + ); + + return await retry.tryForTime(tryTimeout, async () => { + const element = await this.find(selector, findTimeout); return await element.getAttribute(attribute); }); } diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 057ae0bd13b6e..2c71fd8ef8f55 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -57,6 +57,7 @@ import { import { ListingTableProvider } from './listing_table'; import { SavedQueryManagementComponentProvider } from './saved_query_management_component'; import { KibanaSupertestProvider } from './supertest'; +import { MenuToggleProvider } from './menu_toggle'; export const services = { ...commonServiceProviders, @@ -93,4 +94,5 @@ export const services = { elasticChart: ElasticChartProvider, supertest: KibanaSupertestProvider, managementMenu: ManagementMenuProvider, + MenuToggle: MenuToggleProvider, }; diff --git a/test/functional/services/menu_toggle.ts b/test/functional/services/menu_toggle.ts new file mode 100644 index 0000000000000..073aa846f47a8 --- /dev/null +++ b/test/functional/services/menu_toggle.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function MenuToggleProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + interface Options { + name: string; + menuTestSubject: string; + toggleButtonTestSubject: string; + } + + return class MenuToggle { + private readonly name: string; + private readonly menuTestSubject: string; + private readonly toggleButtonTestSubject: string; + + constructor(options: Options) { + this.name = options.name; + this.menuTestSubject = options.menuTestSubject; + this.toggleButtonTestSubject = options.toggleButtonTestSubject; + } + + async open() { + await this.setState(true); + } + + async close() { + await this.setState(false); + } + + private async setState(expectedState: boolean) { + log.debug( + `setting menu open state [name=${this.name}] [state=${expectedState ? 'open' : 'closed'}]` + ); + + await retry.try(async () => { + // if the menu is clearly in the expected state already, bail out quickly if so + const isOpen = await testSubjects.exists(this.menuTestSubject, { timeout: 1000 }); + if (isOpen === expectedState) { + return; + } + + // toggle the view state by clicking the button + await testSubjects.click(this.toggleButtonTestSubject); + + if (expectedState === true) { + // wait for up to 10 seconds for the menu to show up, otherwise fail and retry + await testSubjects.existOrFail(this.menuTestSubject, { timeout: 10000 }); + } else { + // wait for the form to hide, otherwise fail and retry + await testSubjects.waitForDeleted(this.menuTestSubject); + } + }); + } + }; +} diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 550b3b5df12be..4870694e6adbc 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"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":{"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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"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":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"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":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index cf488ac7f3ffa..dd779800cd452 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 8c272901c4e84..992d667fdce9f 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index abc0d3a446987..031c9f9ea5504 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index 1809df5e709f0..8c6fde201c8f1 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index ec32b07ed9f2e..14c8428c6d432 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"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":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index 09602eca4abf2..595127526156e 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"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":{"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":{"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 +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"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/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 550b3b5df12be..4870694e6adbc 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"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":{"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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"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":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 071172c698ad7..073fca760b9a2 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index ad38bb28b3329..93f8d8a27d233 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 997285adfe5f4..e8c47efdbe622 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 10e23d860637c..38683082975f8 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index 550b3b5df12be..4870694e6adbc 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"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":{"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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"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":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"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":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index cf488ac7f3ffa..dd779800cd452 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 8c272901c4e84..992d667fdce9f 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index abc0d3a446987..031c9f9ea5504 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index 1809df5e709f0..8c6fde201c8f1 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index ec32b07ed9f2e..14c8428c6d432 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"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":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":"metric"}} \ 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 index 09602eca4abf2..595127526156e 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"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":{"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":{"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 +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"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/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index 550b3b5df12be..4870694e6adbc 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"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":{"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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 59de1f285799b..2aa601a8d3631 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":{"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":{"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":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"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":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 071172c698ad7..073fca760b9a2 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index ad38bb28b3329..93f8d8a27d233 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index 997285adfe5f4..e8c47efdbe622 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index 10e23d860637c..38683082975f8 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"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":{"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":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"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"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json index a6cc2d34639b7..2949a764d4b1a 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,29 +2,19 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "tsBuildInfoFile": "../build/tsbuildinfo/test", - "types": [ - "node", - "mocha", - "flot" - ] + "types": ["node", "mocha", "flot"] }, - "include": [ - "**/*", - "../typings/elastic__node_crypto.d.ts", - "typings/**/*" - ], - "exclude": [ - "plugin_functional/plugins/**/*", - "interpreter_functional/plugins/**/*" - ], + "include": ["**/*", "../typings/elastic__node_crypto.d.ts", "typings/**/*"], + "exclude": ["plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"], "references": [ { "path": "../src/core/tsconfig.json" }, - { "path": "../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../src/plugins/inspector/tsconfig.json" }, { "path": "../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../src/plugins/newsfeed/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, - { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "../src/plugins/newsfeed/tsconfig.json" } + { "path": "../src/plugins/usage_collection/tsconfig.json" } ] } diff --git a/tsconfig.json b/tsconfig.json index 73646291e3d08..30b38d0fc2dd3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,16 +6,18 @@ "include": ["kibana.d.ts", "src/**/*", "typings/**/*", "test_utils/**/*"], "exclude": [ "src/**/__fixtures__/**/*", - "src/test_utils/**/*", "src/core/**/*", + "src/plugins/inspector/**/*", "src/plugins/kibana_legacy/**/*", - "src/plugins/kibana_utils/**/*", "src/plugins/kibana_react/**/*", - "src/plugins/usage_collection/**/*", + "src/plugins/kibana_usage_collection/**/*", + "src/plugins/kibana_utils/**/*", + "src/plugins/newsfeed/**/*", "src/plugins/telemetry_collection_manager/**/*", "src/plugins/telemetry/**/*", - "src/plugins/kibana_usage_collection/**/*", - "src/plugins/newsfeed/**/*" + "src/plugins/url_forwarding/**/*", + "src/plugins/usage_collection/**/*", + "src/test_utils/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find @@ -23,14 +25,15 @@ // "src/**/public/**/*" ], "references": [ - { "path": "./src/test_utils/tsconfig.json" }, { "path": "./src/core/tsconfig.json" }, - { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/inspector/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, - { "path": "./src/plugins/usage_collection/tsconfig.json" }, + { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/newsfeed/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, - { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "./src/plugins/newsfeed/tsconfig.json" } + { "path": "./src/plugins/usage_collection/tsconfig.json" }, + { "path": "./src/test_utils/tsconfig.json" } ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json index bb1bdc08cafd6..c16e7a5e1b0f1 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -1,14 +1,17 @@ { "include": [], "references": [ - { "path": "./src/test_utils/tsconfig.json" }, { "path": "./src/core/tsconfig.json" }, - { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/inspector/tsconfig.json" }, + { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, { "path": "./src/plugins/kibana_react/tsconfig.json" }, - { "path": "./src/plugins/usage_collection/tsconfig.json" }, - { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/telemetry/tsconfig.json" }, + { "path": "./src/plugins/url_forwarding/tsconfig.json" }, + { "path": "./src/plugins/usage_collection/tsconfig.json" }, + { "path": "./src/test_utils/tsconfig.json" } ] } diff --git a/typings/index.d.ts b/typings/index.d.ts index db6530d3f9e0b..fc16aa32fe3f9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -34,22 +34,3 @@ declare module '*.svg' { // eslint-disable-next-line import/no-default-export export default content; } - -type MethodKeysOf = { - [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; -}[keyof T]; - -type PublicMethodsOf = Pick>; - -type MockedKeys = { [P in keyof T]: jest.Mocked }; - -type DeeplyMockedKeys = { - [P in keyof T]: T[P] extends (...args: any[]) => any - ? jest.MockInstance, Parameters> - : DeeplyMockedKeys; -} & - T; - -type Writable = { - -readonly [K in keyof T]: T[K]; -}; diff --git a/vars/getCheckoutInfo.groovy b/vars/getCheckoutInfo.groovy index 32a7b054bfd15..f9d797f8127c7 100644 --- a/vars/getCheckoutInfo.groovy +++ b/vars/getCheckoutInfo.groovy @@ -2,6 +2,7 @@ def call(branchOverride) { def repoInfo = [ branch: branchOverride ?: env.ghprbSourceBranch, targetBranch: env.ghprbTargetBranch, + targetsTrackedBranch: true ] if (repoInfo.branch == null) { @@ -35,6 +36,10 @@ def call(branchOverride) { label: "determining merge point with '${repoInfo.targetBranch}' at origin", returnStdout: true ).trim() + + def pkgJson = readFile("package.json") + def releaseBranch = toJSON(pkgJson).branch + repoInfo.targetsTrackedBranch = releaseBranch == repoInfo.targetBranch } print "repoInfo: ${repoInfo}" diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index fd5412c905683..546a6785ac2f4 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -149,7 +149,7 @@ def getTestFailuresMessage() { def getBuildStatusIncludingMetrics() { def status = buildUtils.getBuildStatus() - if (status == 'SUCCESS' && !ciStats.getMetricsSuccess()) { + if (status == 'SUCCESS' && shouldCheckCiMetricSuccess() && !ciStats.getMetricsSuccess()) { return 'FAILURE' } @@ -297,3 +297,12 @@ def getFailedSteps() { step.displayName != 'Check out from version control' } } + +def shouldCheckCiMetricSuccess() { + // disable ciMetrics success check when a PR is targetting a non-tracked branch + if (buildState.has('checkoutInfo') && !buildState.get('checkoutInfo').targetsTrackedBranch) { + return false + } + + return true +} diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 2b712a206bfc4..8993213d91f23 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -27,7 +27,7 @@ "xpack.idxMgmt": "plugins/index_management", "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", - "xpack.ingestManager": "plugins/ingest_manager", + "xpack.fleet": "plugins/ingest_manager", "xpack.ingestPipelines": "plugins/ingest_pipelines", "xpack.lens": "plugins/lens", "xpack.licenseMgmt": "plugins/license_management", @@ -56,9 +56,7 @@ "xpack.watcher": "plugins/watcher", "xpack.observability": "plugins/observability" }, - "exclude": [ - "examples" - ], + "exclude": ["examples"], "translations": [ "plugins/translations/translations/zh-CN.json", "plugins/translations/translations/ja-JP.json" diff --git a/x-pack/package.json b/x-pack/package.json index 77dc2e662dd28..ba464a21263d7 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -183,6 +183,7 @@ "jest": "^26.4.2", "jest-circus": "^26.4.2", "jest-cli": "^26.4.2", + "jest-silent-reporter": "^0.2.1", "jest-styled-components": "^7.0.2", "js-search": "^1.4.3", "jsdom": "13.1.0", @@ -258,7 +259,7 @@ "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", "whatwg-fetch": "^3.0.0", - "xml-crypto": "^1.4.0", + "xml-crypto": "^2.0.0", "yargs": "^15.4.1" }, "dependencies": { diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 0c16c88ad7a89..2c2a818eb6eb2 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ActionsClient } from './actions_client'; type ActionsClientContract = PublicMethodsOf; diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts index 6b55c18241c55..59887f405a7dc 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ActionsAuthorization } from './actions_authorization'; export type ActionsAuthorizationMock = jest.Mocked>; diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 23ce527d4ae0d..74feb8ee57d48 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -90,8 +90,9 @@ describe('config validation', () => { }; test('config validation passes when only required fields are provided', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', + hasAuth: true, }; expect(validateConfig(actionType, config)).toEqual({ ...defaultValues, @@ -101,9 +102,10 @@ describe('config validation', () => { test('config validation passes when valid methods are provided', () => { ['post', 'put'].forEach((method) => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', method, + hasAuth: true, }; expect(validateConfig(actionType, config)).toEqual({ ...defaultValues, @@ -127,8 +129,9 @@ describe('config validation', () => { }); test('config validation passes when a url is specified', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', + hasAuth: true, }; expect(validateConfig(actionType, config)).toEqual({ ...defaultValues, @@ -155,6 +158,7 @@ describe('config validation', () => { headers: { 'Content-Type': 'application/json', }, + hasAuth: true, }; expect(validateConfig(actionType, config)).toEqual({ ...defaultValues, @@ -184,6 +188,7 @@ describe('config validation', () => { headers: { 'Content-Type': 'application/json', }, + hasAuth: true, }; expect(validateConfig(actionType, config)).toEqual({ @@ -263,6 +268,7 @@ describe('execute()', () => { headers: { aheader: 'a value', }, + hasAuth: true, }; await actionType.executor({ actionId: 'some-id', @@ -320,6 +326,7 @@ describe('execute()', () => { headers: { aheader: 'a value', }, + hasAuth: false, }; const secrets: ActionTypeSecretsType = { user: null, password: null }; await actionType.executor({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index d0ec31721685e..dc9de86d3d951 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -42,6 +42,7 @@ const configSchemaProps = { defaultValue: WebhookMethods.POST, }), headers: nullableType(HeadersSchema), + hasAuth: schema.boolean({ defaultValue: true }), }; const ConfigSchema = schema.object(configSchemaProps); export type ActionTypeConfigType = TypeOf; @@ -128,12 +129,12 @@ export async function executor( execOptions: WebhookActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; - const { method, url, headers = {} } = execOptions.config; + const { method, url, headers = {}, hasAuth } = execOptions.config; const { body: data } = execOptions.params; const secrets: ActionTypeSecretsType = execOptions.secrets; const basicAuth = - isString(secrets.user) && isString(secrets.password) + hasAuth && isString(secrets.user) && isString(secrets.password) ? { auth: { username: secrets.user, password: secrets.password } } : {}; diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index bbe298c0585a3..39bfe2c2820e2 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { ActionsPlugin } from './plugin'; import { configSchema } from './config'; diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 0015b417d72ce..af70fbf2ec896 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Logger, KibanaRequest } from 'src/core/server'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { diff --git a/x-pack/plugins/actions/server/lib/license_state.ts b/x-pack/plugins/actions/server/lib/license_state.ts index 902fadb3da170..e8cd0aa841cd9 100644 --- a/x-pack/plugins/actions/server/lib/license_state.ts +++ b/x-pack/plugins/actions/server/lib/license_state.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Observable, Subscription } from 'rxjs'; import { assertNever } from '@kbn/std'; import { ILicense } from '../../../licensing/common/types'; diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 18cbd9f9c5fad..c8e2684651598 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -145,6 +145,12 @@ test('executes the task by calling the executor with proper parameters', async ( url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }, }); }); @@ -271,6 +277,12 @@ test('uses API key when provided', async () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }, }); }); @@ -310,6 +322,12 @@ test(`doesn't use API key when not provided`, async () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }, }); }); diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index aeeeb4ed7d520..93ae5a2c807f9 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -102,6 +102,12 @@ export class TaskRunnerFactory { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; let executorResult: ActionTypeExecutorResult; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 668e8b849b8a7..d0c7bf350504b 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { first, map } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { diff --git a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts index 06f65ff23106c..7b8b5f7b7f0e2 100644 --- a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts @@ -6,6 +6,7 @@ import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { identity } from 'lodash'; +import type { MethodKeysOf } from '@kbn/utility-types'; import { httpServerMock } from '../../../../../src/core/server/mocks'; import { ActionType } from '../../common'; import { ActionsClientMock, actionsClientMock } from '../actions_client.mock'; diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/migrations.test.ts index 1fa5889e77cb0..f1bd1ba2aeb60 100644 --- a/x-pack/plugins/actions/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/migrations.test.ts @@ -24,7 +24,11 @@ describe('7.10.0', () => { test('add hasAuth config property for .email actions', () => { const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; const action = getMockDataForEmail({}); - expect(migration710(action, context)).toMatchObject({ + const migratedAction = migration710(action, context); + expect(migratedAction.attributes.config).toEqual({ + hasAuth: true, + }); + expect(migratedAction).toEqual({ ...action, attributes: { ...action.attributes, @@ -38,7 +42,11 @@ describe('7.10.0', () => { test('rename cases configuration object', () => { const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; const action = getMockData({}); - expect(migration710(action, context)).toMatchObject({ + const migratedAction = migration710(action, context); + expect(migratedAction.attributes.config).toEqual({ + incidentConfiguration: { mapping: [] }, + }); + expect(migratedAction).toEqual({ ...action, attributes: { ...action.attributes, @@ -50,6 +58,63 @@ describe('7.10.0', () => { }); }); +describe('7.11.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('add hasAuth = true for .webhook actions with user and password', () => { + const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0']; + const action = getMockDataForWebhook({}, true); + expect(migration711(action, context)).toMatchObject({ + ...action, + attributes: { + ...action.attributes, + config: { + hasAuth: true, + }, + }, + }); + }); + + test('add hasAuth = false for .webhook actions without user and password', () => { + const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0']; + const action = getMockDataForWebhook({}, false); + expect(migration711(action, context)).toMatchObject({ + ...action, + attributes: { + ...action.attributes, + config: { + hasAuth: false, + }, + }, + }); + }); +}); + +function getMockDataForWebhook( + overwrites: Record = {}, + hasUserAndPassword: boolean +): SavedObjectUnsanitizedDoc { + const secrets = hasUserAndPassword + ? { user: 'test', password: '123' } + : { user: '', password: '' }; + return { + attributes: { + name: 'abc', + actionTypeId: '.webhook', + config: {}, + secrets, + ...overwrites, + }, + id: uuid.v4(), + type: 'action', + }; +} + function getMockDataForEmail( overwrites: Record = {} ): SavedObjectUnsanitizedDoc { diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.ts b/x-pack/plugins/actions/server/saved_objects/migrations.ts index 993beef8d9b2b..1e2290b14ec1b 100644 --- a/x-pack/plugins/actions/server/saved_objects/migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/migrations.ts @@ -25,8 +25,18 @@ export function getMigrations( pipeMigrations(renameCasesConfigurationObject, addHasAuthConfigurationObject) ); + const migrationWebhookConnectorHasAuth = encryptedSavedObjects.createMigration< + RawAction, + RawAction + >( + (doc): doc is SavedObjectUnsanitizedDoc => + doc.attributes.actionTypeId === '.webhook', + pipeMigrations(addHasAuthConfigurationObject) + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationActions, '7.10.0'), + '7.11.0': executeMigrationWithErrorHandling(migrationWebhookConnectorHasAuth, '7.11.0'), }; } @@ -70,6 +80,9 @@ function renameCasesConfigurationObject( const addHasAuthConfigurationObject = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc => { + if (doc.attributes.actionTypeId !== '.email' && doc.attributes.actionTypeId !== '.webhook') { + return doc; + } const hasAuth = !!doc.attributes.secrets.user || !!doc.attributes.secrets.password; return { ...doc, diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index a8db8bfd7344c..1867815bd5f90 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ActionTypeRegistry } from './action_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { ActionsClient } from './actions_client'; diff --git a/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.mock.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.mock.ts index 792bd8e885ea6..c800579fddffd 100644 --- a/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.mock.ts +++ b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { AlertNavigationRegistry } from './alert_navigation_registry'; type Schema = PublicMethodsOf; diff --git a/x-pack/plugins/alerts/server/alert_type_registry.mock.ts b/x-pack/plugins/alerts/server/alert_type_registry.mock.ts index b6b36e4d4b7e0..39d15eba014c9 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.mock.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { AlertTypeRegistry } from './alert_type_registry'; type Schema = PublicMethodsOf; diff --git a/x-pack/plugins/alerts/server/alerts_client.mock.ts b/x-pack/plugins/alerts/server/alerts_client.mock.ts index b28e9f805f725..c9063457fac80 100644 --- a/x-pack/plugins/alerts/server/alerts_client.mock.ts +++ b/x-pack/plugins/alerts/server/alerts_client.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { AlertsClient } from './alerts_client'; type Schema = PublicMethodsOf; diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 55c2f3ddd18a4..4e457cdb12bd3 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -60,6 +60,12 @@ const fakeRequest = ({ url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: () => savedObjectsClient, } as unknown) as Request; diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts index 3728daa946d5b..171e3978d0d0d 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { AlertsAuthorization } from './alerts_authorization'; type Schema = PublicMethodsOf; diff --git a/x-pack/plugins/alerts/server/index.ts b/x-pack/plugins/alerts/server/index.ts index 72d3a65220565..1e442c5196cf2 100644 --- a/x-pack/plugins/alerts/server/index.ts +++ b/x-pack/plugins/alerts/server/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { AlertsClient as AlertsClientClass } from './alerts_client'; import { PluginInitializerContext } from '../../../../src/core/server'; import { AlertingPlugin } from './plugin'; diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index b13a1c62f6602..ece7d31d2f7fd 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -149,6 +149,12 @@ describe('Alerting Plugin', () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: jest.fn(), } as unknown) as KibanaRequest; await startContract.getAlertsClientWithRequest(fakeRequest); diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 03302d5e6e7db..565b2992b1f7a 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { first, map } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { SecurityPluginSetup } from '../../security/server'; diff --git a/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts index 548495866ec21..3d13fc65ab260 100644 --- a/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts @@ -11,6 +11,7 @@ import { ILegacyClusterClient, } from 'kibana/server'; import { identity } from 'lodash'; +import type { MethodKeysOf } from '@kbn/utility-types'; import { httpServerMock } from '../../../../../src/core/server/mocks'; import { alertsClientMock, AlertsClientMock } from '../alerts_client.mock'; import { AlertType } from '../../common'; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index d9af3d0ae6d5b..17d5fcd31b745 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -364,6 +364,12 @@ describe('Task Runner', () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` @@ -621,9 +627,7 @@ describe('Task Runner', () => { expect(await taskRunner.run()).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:00:10.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith( @@ -664,6 +668,12 @@ describe('Task Runner', () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }); }); @@ -696,6 +706,12 @@ describe('Task Runner', () => { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, }); }); @@ -727,9 +743,7 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:00:10.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); @@ -781,9 +795,7 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:05:00.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); }); @@ -814,9 +826,7 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:05:00.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); }); @@ -846,13 +856,48 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:05:00.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); }); + test(`doesn't change previousStartedAt when it fails to run`, async () => { + const originalAlertSate = { + previousStartedAt: '1970-01-05T00:00:00.000Z', + }; + + alertType.executor.mockImplementation( + ({ services: executorServices }: AlertExecutorOptions) => { + throw new Error('OMG'); + } + ); + + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: originalAlertSate, + }, + taskRunnerFactoryInitializerParams + ); + + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + const runnerResult = await taskRunner.run(); + + expect(runnerResult.state.previousStartedAt).toEqual( + new Date(originalAlertSate.previousStartedAt) + ); + }); + test('avoids rescheduling a failed Alert Task Runner when it throws due to failing to fetch the alert', async () => { alertsClient.get.mockImplementation(() => { throw SavedObjectsErrorHelpers.createGenericNotFoundError('task', '1'); @@ -878,9 +923,7 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": undefined, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); }); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 1ccf14a3a5334..76125da20d552 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { pickBy, mapValues, without } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; @@ -101,6 +101,12 @@ export class TaskRunner { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; } @@ -322,7 +328,7 @@ export class TaskRunner { async run(): Promise { const { params: { alertId, spaceId }, - startedAt: previousStartedAt, + startedAt, state: originalState, } = this.taskInstance; @@ -360,7 +366,7 @@ export class TaskRunner { (stateUpdates: AlertTaskState) => { return { ...stateUpdates, - previousStartedAt, + previousStartedAt: startedAt, }; }, (err: Error) => { @@ -370,10 +376,7 @@ export class TaskRunner { } else { this.logger.error(message); } - return { - ...originalState, - previousStartedAt, - }; + return originalState; } ), runAt: resolveErr(runAt, (err) => { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index 944c4dc64ce7a..df6f306c6ccc5 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Logger, KibanaRequest, ISavedObjectsRepository } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index 74153d1ca6b1d..42eef9bba10e5 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { AlertInstance } from './alert_instance'; import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 02456f9b2050f..6edf56fb9a1ae 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -91,3 +91,5 @@ export function isSpanGroupingSupported(type?: string, subtype?: string) { nongroupedSubType === 'all' || nongroupedSubType === subtype ); } + +export const SERVICE_MAP_TIMEOUT_ERROR = 'ServiceMapTimeoutError'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/shared/attempt_to_uri_decode.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts similarity index 55% rename from x-pack/plugins/ingest_pipelines/public/application/sections/shared/attempt_to_uri_decode.ts rename to x-pack/plugins/apm/common/ui_settings_keys.ts index fe5a0d7932cbb..38922fa445a47 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/shared/attempt_to_uri_decode.ts +++ b/x-pack/plugins/apm/common/ui_settings_keys.ts @@ -4,12 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export const attemptToURIDecode = (value: string) => { - let result: string; - try { - result = decodeURI(decodeURIComponent(value)); - } catch (e) { - result = value; - } - return result; -}; +export const enableCorrelations = 'apm:enableCorrelations'; +export const enableServiceOverview = 'apm:enableServiceOverview'; diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js index c37326ee5b196..d77d3ba0bec0f 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -1,3 +1,3 @@ module.exports = { - "__version": "4.9.0" + "__version": "5.5.0" } diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts index a8edf862ab256..d8540c3f3efd7 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts @@ -26,7 +26,7 @@ Given(`a user browses the APM UI application for RUM Data`, () => { }); Then(`should have correct client metrics`, () => { - const metrics = ['4 ms', '58 ms', '55']; + const metrics = ['80 ms', '4 ms', '76 ms', '55']; verifyClientMetrics(metrics, true); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts index 5c2109bb518c2..88287286c66c5 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts @@ -56,7 +56,9 @@ Then(/^it filters the client metrics "([^"]*)"$/, (filterName) => { cy.get('.euiStat__title-isLoading').should('not.be.visible'); const data = - filterName === 'os' ? ['5 ms', '64 ms', '8'] : ['4 ms', '55 ms', '28']; + filterName === 'os' + ? ['82 ms', '5 ms', '77 ms', '8'] + : ['75 ms', '4 ms', '71 ms', '28']; verifyClientMetrics(data, true); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/js_errors.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/js_errors.ts index 0dbefb73e68c4..9e10e2fd59914 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/js_errors.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/js_errors.ts @@ -12,12 +12,7 @@ Then(`it displays list of relevant js errors`, () => { cy.get('.euiBasicTable-loading').should('not.be.visible'); cy.get('.euiStat__title-isLoading').should('not.be.visible'); - getDataTestSubj('uxJsErrorsTotal').should('have.text', 'Total errors110'); - - getDataTestSubj('uxJsErrorRate').should( - 'have.text', - 'Error rate100 %Error rate 100 %' - ); + getDataTestSubj('uxJsErrorsTotal').should('have.text', 'Total errors112'); getDataTestSubj('uxJsErrorTable').within(() => { cy.get('tr.euiTableRow', DEFAULT_TIMEOUT) diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts index 314254883b2fd..44802bbce6208 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts @@ -16,7 +16,7 @@ When('the user changes the selected percentile', () => { }); Then(`it displays client metric related to that percentile`, () => { - const metrics = ['14 ms', '131 ms', '55']; + const metrics = ['165 ms', '14 ms', '151 ms', '55']; verifyClientMetrics(metrics, false); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts index 20c6a3fb72aa9..609d0d18f5bc8 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts @@ -15,7 +15,7 @@ When('the user changes the selected service name', () => { }); Then(`it displays relevant client metrics`, () => { - const metrics = ['4 ms', '58 ms', '55']; + const metrics = ['80 ms', '4 ms', '76 ms', '55']; verifyClientMetrics(metrics, false); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts index 75b9a9c804168..3dc98625baf85 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts @@ -24,8 +24,8 @@ Then(`it displays top pages in the suggestion popover`, () => { listOfUrls.should('have.length', 5); const actualUrlsText = [ - 'http://opbeans-node:3000/dashboardPage views: 17Page load duration: 109 ms', - 'http://opbeans-node:3000/ordersPage views: 14Page load duration: 72 ms', + 'http://opbeans-node:3000/dashboardTotal page views: 17Page load duration: 109 ms (median)', + 'http://opbeans-node:3000/ordersTotal page views: 14Page load duration: 72 ms (median)', ]; cy.get('li.euiSelectableListItem') @@ -55,7 +55,7 @@ Then(`it should filter results based on query`, () => { listOfUrls.should('have.length', 1); const actualUrlsText = [ - 'http://opbeans-node:3000/customersPage views: 10Page load duration: 76 ms', + 'http://opbeans-node:3000/customersTotal page views: 10Page load duration: 76 ms (median)', ]; cy.get('li.euiSelectableListItem') diff --git a/x-pack/plugins/apm/e2e/yarn.lock b/x-pack/plugins/apm/e2e/yarn.lock index fc63189e97ea3..87331c72e8025 100644 --- a/x-pack/plugins/apm/e2e/yarn.lock +++ b/x-pack/plugins/apm/e2e/yarn.lock @@ -689,14 +689,6 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" -"@babel/runtime-corejs3@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz#f29fc1990307c4c57b10dbd6ce667b27159d9e0d" - integrity sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw== - dependencies: - core-js-pure "^3.0.0" - regenerator-runtime "^0.13.4" - "@babel/runtime@7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a" @@ -760,7 +752,7 @@ lodash.clonedeep "4.5.0" watchify "3.11.1" -"@cypress/listr-verbose-renderer@0.4.1": +"@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo= @@ -770,7 +762,7 @@ date-fns "^1.27.2" figures "^1.7.0" -"@cypress/request@2.88.5": +"@cypress/request@^2.88.5": version "2.88.5" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== @@ -819,7 +811,7 @@ debug "4.1.1" lodash "4.17.15" -"@cypress/xvfb@1.2.4": +"@cypress/xvfb@^1.2.4": version "1.2.4" resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== @@ -884,22 +876,22 @@ resolved "https://registry.yarnpkg.com/@types/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.14.1.tgz#9787f4e89553ebc6359ce157a26ad51ed14aa98b" integrity sha512-CpYsiQ49UrOmadhFg0G5RkokPUmGGctD01mOWjNxFxHw5VgIRv33L2RyFHL8klaAI4HaedGN3Tcj4HTQ65hn+A== -"@types/node@^14.0.14": - version "14.0.14" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" - integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== +"@types/node@>=10.17.17 <10.20.0": + version "10.17.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.43.tgz#af70c6de04841d9216fc2b58e427c8a823b83418" + integrity sha512-F7xV2kxZGb3seVP3UQt3msHcoDCtDi8WNO/UCzNLhRwaYVT4yJO1ndcV+vCTnY+jiAVqyLZq/VJbRE/AhwqEag== "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/sinonjs__fake-timers@6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" - integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== +"@types/sinonjs__fake-timers@^6.0.1": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae" + integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg== -"@types/sizzle@2.3.2": +"@types/sizzle@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -1189,15 +1181,23 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -arch@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" - integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== +arch@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== argparse@^1.0.7: version "1.0.10" @@ -1289,6 +1289,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -1376,6 +1381,11 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1383,6 +1393,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +blob-util@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + bluebird@3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" @@ -1393,7 +1408,7 @@ bluebird@3.7.1: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de" integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg== -bluebird@3.7.2, bluebird@^3.4.1, bluebird@^3.5.5: +bluebird@^3.4.1, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -1427,7 +1442,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1712,7 +1727,7 @@ cached-path-relative@^1.0.0, cached-path-relative@^1.0.2: resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" integrity sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg== -cachedir@2.3.0: +cachedir@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== @@ -1744,15 +1759,6 @@ chai@^4.1.2: pathval "^1.1.0" type-detect "^4.0.5" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -1764,6 +1770,15 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -1772,12 +1787,20 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -check-more-types@2.24.0: +check-more-types@2.24.0, check-more-types@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= @@ -1801,6 +1824,21 @@ chokidar@^2.0.4, chokidar@^2.1.1, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" + integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.5.0" + optionalDependencies: + fsevents "~2.1.2" + chownr@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" @@ -1862,13 +1900,13 @@ cli-spinners@^2.2.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== -cli-table3@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" - integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== +cli-table3@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" + integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== dependencies: object-assign "^4.1.0" - string-width "^2.1.1" + string-width "^4.2.0" optionalDependencies: colors "^1.1.2" @@ -1978,17 +2016,17 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - commander@^2.19.0, commander@^2.20.0, commander@^2.9.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -common-tags@1.8.0: +commander@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== @@ -2073,11 +2111,6 @@ core-js-compat@^3.1.1: browserslist "^4.8.3" semver "7.0.0" -core-js-pure@^3.0.0: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" - integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== - core-js@^2.4.0: version "2.6.11" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" @@ -2129,16 +2162,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" crypto-browserify@^3.0.0, crypto-browserify@^3.11.0: version "3.12.0" @@ -2234,48 +2265,49 @@ cypress-cucumber-preprocessor@^2.5.2: minimist "^1.2.0" through "^2.3.8" -cypress@^4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.9.0.tgz#c188a3864ddf841c0fdc81a9e4eff5cf539cd1c1" - integrity sha512-qGxT5E0j21FPryzhb0OBjCdhoR/n1jXtumpFFSBPYWsaZZhNaBvc3XlBUDEZKkkXPsqUFYiyhWdHN/zo0t5FcA== - dependencies: - "@cypress/listr-verbose-renderer" "0.4.1" - "@cypress/request" "2.88.5" - "@cypress/xvfb" "1.2.4" - "@types/sinonjs__fake-timers" "6.0.1" - "@types/sizzle" "2.3.2" - arch "2.1.2" - bluebird "3.7.2" - cachedir "2.3.0" - chalk "2.4.2" - check-more-types "2.24.0" - cli-table3 "0.5.1" - commander "4.1.1" - common-tags "1.8.0" - debug "4.1.1" - eventemitter2 "6.4.2" - execa "1.0.0" - executable "4.1.1" - extract-zip "1.7.0" - fs-extra "8.1.0" - getos "3.2.1" - is-ci "2.0.0" - is-installed-globally "0.3.2" - lazy-ass "1.6.0" - listr "0.14.3" - lodash "4.17.15" - log-symbols "3.0.0" - minimist "1.2.5" - moment "2.26.0" - ospath "1.2.2" - pretty-bytes "5.3.0" - ramda "0.26.1" - request-progress "3.0.0" - supports-color "7.1.0" - tmp "0.1.0" - untildify "4.0.0" - url "0.11.0" - yauzl "2.10.0" +cypress@^5.0.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.5.0.tgz#1da0355794a43247f8a80cb7f505e83e1cf847cb" + integrity sha512-UHEiTca8AUTevbT2pWkHQlxoHtXmbq+h6Eiu/Mz8DqpNkF98zjTBLv/HFiKJUU5rQzp9EwSWtms33p5TWCJ8tQ== + dependencies: + "@cypress/listr-verbose-renderer" "^0.4.1" + "@cypress/request" "^2.88.5" + "@cypress/xvfb" "^1.2.4" + "@types/sinonjs__fake-timers" "^6.0.1" + "@types/sizzle" "^2.3.2" + arch "^2.1.2" + blob-util "2.0.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-table3 "~0.6.0" + commander "^4.1.1" + common-tags "^1.8.0" + debug "^4.1.1" + eventemitter2 "^6.4.2" + execa "^4.0.2" + executable "^4.1.1" + extract-zip "^1.7.0" + fs-extra "^9.0.1" + getos "^3.2.1" + is-ci "^2.0.0" + is-installed-globally "^0.3.2" + lazy-ass "^1.6.0" + listr "^0.14.3" + lodash "^4.17.19" + log-symbols "^4.0.0" + minimist "^1.2.5" + moment "^2.27.0" + ospath "^1.2.2" + pretty-bytes "^5.4.1" + ramda "~0.26.1" + request-progress "^3.0.0" + supports-color "^7.2.0" + tmp "~0.2.1" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" d@1, d@^1.0.1: version "1.0.1" @@ -2330,18 +2362,18 @@ debug@^3.0.1, debug@^3.1.0: dependencies: ms "^2.1.1" +debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decamelize@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-3.2.0.tgz#84b8e8f4f8c579f938e35e2cc7024907e0090851" - integrity sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw== - dependencies: - xregexp "^4.2.4" - decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -2542,7 +2574,7 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: +enhanced-resolve@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== @@ -2551,6 +2583,15 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: memory-fs "^0.5.0" tapable "^1.0.0" +enhanced-resolve@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + errno@^0.1.3, errno@~0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" @@ -2633,10 +2674,10 @@ esutils@^2.0.0, esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eventemitter2@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.2.tgz#f31f8b99d45245f0edbc5b00797830ff3b388970" - integrity sha512-r/Pwupa5RIzxIHbEKCkNXqpEQIIT4uQDxmP4G/Lug/NokVUWj0joz/WzWl3OxRpC5kDrH/WdiUJoR+IrwvXJEw== +eventemitter2@^6.4.2: + version "6.4.3" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" + integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ== events@^2.0.0: version "2.1.0" @@ -2656,20 +2697,22 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -execa@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" +execa@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2" + integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" -executable@4.1.1: +executable@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== @@ -2735,7 +2778,7 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@1.7.0: +extract-zip@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== @@ -2911,14 +2954,15 @@ fs-extra@7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== +fs-extra@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== dependencies: + at-least-node "^1.0.0" graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" + jsonfile "^6.0.1" + universalify "^1.0.0" fs-write-stream-atomic@^1.0.8: version "1.0.10" @@ -2943,6 +2987,11 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -2963,10 +3012,10 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== dependencies: pump "^3.0.0" @@ -2975,7 +3024,7 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -getos@3.2.1: +getos@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== @@ -3002,6 +3051,13 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob@^7.0.0, glob@^7.1.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -3148,6 +3204,11 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -3257,12 +3318,19 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.0, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-ci@2.0.0: +is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== @@ -3352,14 +3420,14 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== dependencies: is-extglob "^2.1.1" -is-installed-globally@0.3.2: +is-installed-globally@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== @@ -3413,6 +3481,11 @@ is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -3557,6 +3630,15 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" + integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== + dependencies: + universalify "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -3616,7 +3698,7 @@ labeled-stream-splicer@^2.0.0: inherits "^2.0.1" stream-splicer "^2.0.0" -lazy-ass@1.6.0: +lazy-ass@1.6.0, lazy-ass@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= @@ -3650,7 +3732,7 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr@0.14.3: +listr@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== @@ -3714,12 +3796,10 @@ lodash@4.17.15, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17. resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -log-symbols@3.0.0, log-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== - dependencies: - chalk "^2.4.2" +lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== log-symbols@^1.0.2: version "1.0.2" @@ -3728,6 +3808,20 @@ log-symbols@^1.0.2: dependencies: chalk "^1.0.0" +log-symbols@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== + dependencies: + chalk "^2.4.2" + +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + log-update@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" @@ -3809,6 +3903,11 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -3888,16 +3987,16 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@1.2.5, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -3964,10 +4063,10 @@ module-deps@^6.0.0: through2 "^2.0.0" xtend "^4.0.0" -moment@2.26.0: - version "2.26.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" - integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== +moment@^2.27.0: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== move-concurrently@^1.0.1: version "1.0.1" @@ -3986,7 +4085,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.1.1: +ms@2.1.2, ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -4037,11 +4136,6 @@ next-tick@~1.0.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - no-case@^2.2.0: version "2.3.2" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" @@ -4100,17 +4194,17 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: - path-key "^2.0.0" + path-key "^3.0.0" number-is-nan@^1.0.0: version "1.0.1" @@ -4228,7 +4322,7 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -ospath@1.2.2: +ospath@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= @@ -4240,11 +4334,6 @@ outpipe@^1.1.0: dependencies: shell-quote "^1.4.2" -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - p-limit@^2.0.0, p-limit@^2.2.0: version "2.2.2" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" @@ -4369,10 +4458,10 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: version "1.0.6" @@ -4410,6 +4499,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + picomatch@^2.0.5: version "2.2.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" @@ -4437,10 +4531,10 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -pretty-bytes@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" - integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== +pretty-bytes@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.4.1.tgz#cd89f79bbcef21e3d21eb0da68ffe93f803e884b" + integrity sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA== private@^0.1.6: version "0.1.8" @@ -4559,7 +4653,7 @@ ramda@0.25.0: resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9" integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ== -ramda@0.26.1: +ramda@~0.26.1: version "0.26.1" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== @@ -4617,6 +4711,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== + dependencies: + picomatch "^2.2.1" + regenerate-unicode-properties@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" @@ -4639,11 +4740,6 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== -regenerator-runtime@^0.13.4: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== - regenerator-transform@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" @@ -4698,7 +4794,7 @@ repeat-string@^1.5.2, repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -request-progress@3.0.0: +request-progress@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= @@ -4778,6 +4874,13 @@ rimraf@^2.5.4, rimraf@^2.6.3: dependencies: glob "^7.1.3" +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -4911,17 +5014,17 @@ shasum@^1.0.0: json-stable-stringify "~0.0.0" sha.js "~2.4.4" -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: - shebang-regex "^1.0.0" + shebang-regex "^3.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.4.2, shell-quote@^1.6.1: version "1.7.2" @@ -4933,7 +5036,7 @@ sigmund@^1.0.1: resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= -signal-exit@^3.0.0, signal-exit@^3.0.2: +signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= @@ -5253,10 +5356,10 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== subarg@^1.0.0: version "1.0.0" @@ -5265,13 +5368,6 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" -supports-color@7.1.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -5284,6 +5380,20 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -5379,12 +5489,12 @@ title-case@^2.1.1: no-case "^2.2.0" upper-case "^1.0.3" -tmp@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" - integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw== +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== dependencies: - rimraf "^2.6.3" + rimraf "^3.0.0" to-arraybuffer@^1.0.0: version "1.0.1" @@ -5567,6 +5677,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -5575,7 +5690,7 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -untildify@4.0.0: +untildify@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== @@ -5602,7 +5717,7 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= -url@0.11.0, url@^0.11.0, url@~0.11.0: +url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= @@ -5697,14 +5812,23 @@ watchify@3.11.1: through2 "^2.0.0" xtend "^4.0.0" -watchpack@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2" - integrity sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA== +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== dependencies: chokidar "^2.1.8" + +watchpack@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" + integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== + dependencies: graceful-fs "^4.1.2" neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.1" + watchpack-chokidar2 "^2.0.0" wcwidth@^1.0.1: version "1.0.1" @@ -5721,10 +5845,10 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.43.0: - version "4.43.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" - integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== +webpack@^4.41.5: + version "4.44.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" + integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" @@ -5734,7 +5858,7 @@ webpack@^4.43.0: ajv "^6.10.2" ajv-keywords "^3.4.1" chrome-trace-event "^1.0.2" - enhanced-resolve "^4.1.0" + enhanced-resolve "^4.3.0" eslint-scope "^4.0.3" json-parse-better-errors "^1.0.2" loader-runner "^2.4.0" @@ -5747,7 +5871,7 @@ webpack@^4.43.0: schema-utils "^1.0.0" tapable "^1.1.3" terser-webpack-plugin "^1.4.3" - watchpack "^1.6.1" + watchpack "^1.7.4" webpack-sources "^1.4.1" which-module@^2.0.0: @@ -5755,10 +5879,10 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" @@ -5791,13 +5915,6 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xregexp@^4.2.4: - version "4.3.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" - integrity sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g== - dependencies: - "@babel/runtime-corejs3" "^7.8.3" - xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -5826,13 +5943,13 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@^15.4.0: - version "15.4.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.0.tgz#53949fb768309bac1843de9b17b80051e9805ec2" - integrity sha512-D3fRFnZwLWp8jVAAhPZBsmeIHY8tTsb8ItV9KaAaopmC6wde2u6Yw29JBIZHXw14kgkRnYmDgmQU4FVMDlIsWw== +yargs@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== dependencies: cliui "^6.0.0" - decamelize "^3.2.0" + decamelize "^1.2.0" find-up "^4.1.0" get-caller-file "^2.0.1" require-directory "^2.1.1" @@ -5843,7 +5960,7 @@ yargs@^15.4.0: y18n "^4.0.0" yargs-parser "^18.1.2" -yauzl@2.10.0, yauzl@^2.10.0: +yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 2baddbe572a52..d8f54c7bfc94f 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -28,6 +28,7 @@ import { ConfigSchema } from '../index'; import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; +import { createStaticIndexPattern } from '../services/rest/index_pattern'; const CsmMainContainer = styled.div` padding: ${px(units.plus)}; @@ -114,6 +115,12 @@ export const renderApp = ( ) => { createCallApmApi(core.http); + // Automatically creates static index pattern and stores as saved object + createStaticIndexPattern().catch((e) => { + // eslint-disable-next-line no-console + console.log('Error creating static index pattern', e); + }); + ReactDOM.render( ; } +function ServiceDetailsOverview( + props: RouteComponentProps<{ serviceName: string }> +) { + return ; +} + function ServiceDetailsServiceMap( props: RouteComponentProps<{ serviceName: string }> ) { @@ -215,6 +221,14 @@ export const routes: APMRouteDefinition[] = [ `/services/${props.match.params.serviceName}/transactions` )(props), } as APMRouteDefinition<{ serviceName: string }>, + { + exact: true, + path: '/services/:serviceName/overview', + breadcrumb: i18n.translate('xpack.apm.breadcrumb.overviewTitle', { + defaultMessage: 'Overview', + }), + component: ServiceDetailsOverview, + } as APMRouteDefinition<{ serviceName: string }>, // errors { exact: true, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 0b4dcea5d12e0..b6924b9552699 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -7,7 +7,13 @@ import * as React from 'react'; import numeral from '@elastic/numeral'; import styled from 'styled-components'; import { useContext, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiToolTip, + EuiIconTip, +} from '@elastic/eui'; import { useFetcher } from '../../../../hooks/useFetcher'; import { I18LABELS } from '../translations'; import { useUxQuery } from '../hooks/useUxQuery'; @@ -70,11 +76,35 @@ export function ClientMetrics() { return ( + + + {I18LABELS.totalPageLoad} + + + } + isLoading={status !== 'success'} + /> + + {I18LABELS.backEnd} + + + } isLoading={status !== 'success'} /> @@ -82,7 +112,15 @@ export function ClientMetrics() { + {I18LABELS.frontEnd} + + + } isLoading={status !== 'success'} /> diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx index 7a7c051817555..58f00604b8fda 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx @@ -15,7 +15,6 @@ import { EuiToolTip, } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { useFetcher } from '../../../../hooks/useFetcher'; @@ -102,11 +101,6 @@ export function JSErrors() { }); }; - const errorRate = - totalPageViews > 0 - ? ((data?.totalErrorPages ?? 0) / totalPageViews) * 100 - : 0; - const totalErrors = data?.totalErrors ?? 0; return ( @@ -133,20 +127,6 @@ export function JSErrors() { isLoading={status !== 'success'} /> - - - {' '} -

{I18LABELS.pageLoadDuration}

+

+ {I18LABELS.pageLoad} ({getPercentileLabel(percentile!)}) +

diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index d1cfe1d63f88e..f25761bd8abad 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -30,6 +30,7 @@ export function RumHome() { wrap style={{ flexWrap: 'wrap-reverse' }} justifyContent="flexEnd" + gutterSize="s" > diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx index d9d3d23299371..7bd9b2c87814b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx @@ -10,6 +10,7 @@ import React, { useRef, useState, KeyboardEvent, + useEffect, } from 'react'; import { EuiFlexGroup, @@ -67,6 +68,7 @@ interface Props { searchValue: string; onClose: () => void; popoverIsOpen: boolean; + initialValue?: string; setPopoverIsOpen: React.Dispatch>; } @@ -80,6 +82,7 @@ export function SelectableUrlList({ onClose, popoverIsOpen, setPopoverIsOpen, + initialValue, }: Props) { const [darkMode] = useUiSetting$('theme:darkMode'); @@ -92,6 +95,9 @@ export function SelectableUrlList({ if (evt.key.toLowerCase() === 'enter') { onTermChange(); setPopoverIsOpen(false); + if (searchRef) { + searchRef.blur(); + } } }; @@ -126,6 +132,16 @@ export function SelectableUrlList({ } }; + useEffect(() => { + if (searchRef && initialValue) { + searchRef.value = initialValue; + } + + // only want to call it at initial render to set value + // coming from initial value/url + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchRef]); + const loadingMessage = ( @@ -165,12 +181,12 @@ export function SelectableUrlList({ renderOption={selectableRenderOptions} singleSelection={false} searchProps={{ - placeholder: I18LABELS.searchByUrl, isClearable: true, onFocus: searchOnFocus, onBlur: searchOnBlur, onInput: onSearchInput, inputRef: setSearchRef, + placeholder: I18LABELS.searchByUrl, }} listProps={{ rowHeight: 68, @@ -197,7 +213,7 @@ export function SelectableUrlList({ {searchValue}, icon: ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx new file mode 100644 index 0000000000000..abafdf089748b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/__tests__/SelectableUrlList.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { createMemoryHistory } from 'history'; +import * as fetcherHook from '../../../../../../hooks/useFetcher'; +import { SelectableUrlList } from '../SelectableUrlList'; +import { render } from '../../../utils/test_helper'; + +describe('SelectableUrlList', () => { + it('it uses search term value from url', () => { + jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data: {}, + status: fetcherHook.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + + const customHistory = createMemoryHistory({ + initialEntries: ['/?searchTerm=blog'], + }); + + const { getByDisplayValue } = render( + , + { customHistory } + ); + expect(getByDisplayValue('blog')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx index 661f4406990f6..f9aeb484cbdf9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx @@ -16,6 +16,7 @@ import { formatToSec } from '../../UXMetrics/KeyUXMetrics'; import { SelectableUrlList } from './SelectableUrlList'; import { UrlOption } from './RenderOption'; import { useUxQuery } from '../../hooks/useUxQuery'; +import { getPercentileLabel } from '../../UXMetrics/translations'; interface Props { onChange: (value: string[]) => void; @@ -26,13 +27,13 @@ export function URLSearch({ onChange: onFilterChange }: Props) { const { uiFilters, urlParams } = useUrlParams(); - const { searchTerm } = urlParams; + const { searchTerm, percentile } = urlParams; const [popoverIsOpen, setPopoverIsOpen] = useState(false); - const [searchValue, setSearchValue] = useState(''); + const [searchValue, setSearchValue] = useState(searchTerm ?? ''); - const [debouncedValue, setDebouncedValue] = useState(''); + const [debouncedValue, setDebouncedValue] = useState(searchTerm ?? ''); useDebounce( () => { @@ -44,12 +45,16 @@ export function URLSearch({ onChange: onFilterChange }: Props) { const updateSearchTerm = useCallback( (searchTermN: string) => { + const newQuery = { + ...toQuery(history.location.search), + searchTerm: searchTermN || undefined, + }; + if (!searchTermN) { + delete newQuery.searchTerm; + } const newLocation = { ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - searchTerm: searchTermN, - }), + search: fromQuery(newQuery), }; history.push(newLocation); }, @@ -100,12 +105,17 @@ export function URLSearch({ onChange: onFilterChange }: Props) { setCheckedUrls(clickedItems.map((item) => item.url)); }; + const percTitle = getPercentileLabel(percentile!); + const items: UrlOption[] = (data?.items ?? []).map((item) => ({ label: item.url, key: item.url, meta: [ I18LABELS.pageViews + ': ' + item.count, - I18LABELS.pageLoadDuration + ': ' + formatToSec(item.pld), + I18LABELS.pageLoadDuration + + ': ' + + formatToSec(item.pld) + + ` (${percTitle})`, ], url: item.url, checked: checkedUrls?.includes(item.url) ? 'on' : undefined, @@ -133,6 +143,7 @@ export function URLSearch({ onChange: onFilterChange }: Props) {

{I18LABELS.url}

+ {FCP_LABEL} + + + } isLoading={loading} />
@@ -77,7 +88,12 @@ export function KeyUXMetrics({ data, loading }: Props) { + {TBT_LABEL} + + + } isLoading={loading} />
@@ -85,11 +101,19 @@ export function KeyUXMetrics({ data, loading }: Props) { + {NO_OF_LONG_TASK} + + + } isLoading={status !== 'success'} /> @@ -97,7 +121,15 @@ export function KeyUXMetrics({ data, loading }: Props) { + {LONGEST_LONG_TASK} + + + } isLoading={status !== 'success'} /> @@ -105,7 +137,15 @@ export function KeyUXMetrics({ data, loading }: Props) { + {SUM_LONG_TASKS} + + + } isLoading={status !== 'success'} /> diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx index cced16691b712..3a6323a747a70 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/__tests__/KeyUXMetrics.test.tsx @@ -19,7 +19,7 @@ describe('KeyUXMetrics', () => { status: fetcherHook.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); - const { getByText } = render( + const { getAllByText } = render( { lcpRanks: [69, 17, 14], fidRanks: [83, 6, 11], clsRanks: [90, 7, 3], + coreVitalPages: 1000, }} /> ); - expect(getByText('Longest long task duration 271 ms')).toBeInTheDocument(); - expect(getByText('Total long tasks duration 520 ms')).toBeInTheDocument(); - expect(getByText('No. of long tasks 3')).toBeInTheDocument(); - expect(getByText('Total blocking time 271 ms')).toBeInTheDocument(); - expect(getByText('First contentful paint 1.27 s')).toBeInTheDocument(); + const checkText = (text: string) => { + return (content: any, node: any) => { + return node?.textContent?.includes(text); + }; + }; + + expect( + getAllByText(checkText('Longest long task duration271 ms'))[0] + ).toBeInTheDocument(); + expect( + getAllByText(checkText('Total long tasks duration520 ms'))[0] + ).toBeInTheDocument(); + expect( + getAllByText(checkText('No. of long tasks3'))[0] + ).toBeInTheDocument(); + expect( + getAllByText(checkText('Total blocking time271 ms'))[0] + ).toBeInTheDocument(); + expect( + getAllByText(checkText('First contentful paint1.27 s'))[0] + ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 9ff524db30366..983e3be1c21a9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -18,8 +18,15 @@ import { KeyUXMetrics } from './KeyUXMetrics'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUxQuery } from '../hooks/useUxQuery'; import { CoreVitals } from '../../../../../../observability/public'; +import { CsmSharedContext } from '../CsmSharedContext'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { getPercentileLabel } from './translations'; export function UXMetrics() { + const { + urlParams: { percentile }, + } = useUrlParams(); + const uxQuery = useUxQuery(); const { data, status } = useFetcher( @@ -37,12 +44,18 @@ export function UXMetrics() { [uxQuery] ); + const { + sharedData: { totalPageViews }, + } = useContext(CsmSharedContext); + return ( - -

{I18LABELS.userExperienceMetrics}

+ +

+ {I18LABELS.metrics} ({getPercentileLabel(percentile!)}) +

@@ -54,7 +67,12 @@ export function UXMetrics() { - +
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts index 8f3a71f669ecf..3795f2f102237 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/translations.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { I18LABELS } from '../translations'; export const DATA_UNDEFINED_LABEL = i18n.translate( 'xpack.apm.rum.coreVitals.dataUndefined', @@ -17,10 +18,26 @@ export const FCP_LABEL = i18n.translate('xpack.apm.rum.coreVitals.fcp', { defaultMessage: 'First contentful paint', }); +export const FCP_TOOLTIP = i18n.translate( + 'xpack.apm.rum.coreVitals.fcpTooltip', + { + defaultMessage: + 'First contentful paint (FCP) focusses on the initial rendering and measures the time from when the page starts loading to when any part of the page’s content is displayed on the screen.', + } +); + export const TBT_LABEL = i18n.translate('xpack.apm.rum.coreVitals.tbt', { defaultMessage: 'Total blocking time', }); +export const TBT_TOOLTIP = i18n.translate( + 'xpack.apm.rum.coreVitals.tbtTooltip', + { + defaultMessage: + 'Total blocking time (TBT) is the sum of the blocking time (duration above 50 ms) for each long task that occurs between the First contentful paint and the time when the transaction is completed.', + } +); + export const NO_OF_LONG_TASK = i18n.translate( 'xpack.apm.rum.uxMetrics.noOfLongTasks', { @@ -28,6 +45,14 @@ export const NO_OF_LONG_TASK = i18n.translate( } ); +export const NO_OF_LONG_TASK_TOOLTIP = i18n.translate( + 'xpack.apm.rum.uxMetrics.noOfLongTasksTooltip', + { + defaultMessage: + 'The number of long tasks, a long task is defined as any user activity or browser task that monopolizes the UI thread for extended periods (greater than 50 milliseconds) and blocks other critical tasks (frame rate or input latency) from being executed.', + } +); + export const LONGEST_LONG_TASK = i18n.translate( 'xpack.apm.rum.uxMetrics.longestLongTasks', { @@ -35,9 +60,36 @@ export const LONGEST_LONG_TASK = i18n.translate( } ); +export const LONGEST_LONG_TASK_TOOLTIP = i18n.translate( + 'xpack.apm.rum.uxMetrics.longestLongTasksTooltip', + { + defaultMessage: + 'The duration of the longest long task, a long task is defined as any user activity or browser task that monopolizes the UI thread for extended periods (greater than 50 milliseconds) and blocks other critical tasks (frame rate or input latency) from being executed.', + } +); + export const SUM_LONG_TASKS = i18n.translate( 'xpack.apm.rum.uxMetrics.sumLongTasks', { defaultMessage: 'Total long tasks duration', } ); + +export const SUM_LONG_TASKS_TOOLTIP = i18n.translate( + 'xpack.apm.rum.uxMetrics.sumLongTasksTooltip', + { + defaultMessage: + 'The total duration of long tasks, a long task is defined as any user activity or browser task that monopolizes the UI thread for extended periods (greater than 50 milliseconds) and blocks other critical tasks (frame rate or input latency) from being executed.', + } +); + +export const getPercentileLabel = (value: number) => { + if (value === 50) return I18LABELS.median; + + return i18n.translate('xpack.apm.ux.percentiles.label', { + defaultMessage: '{value}th Perc.', + values: { + value, + }, + }); +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx index 18cd7d79cc69f..04c7e3cc00287 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx @@ -45,13 +45,11 @@ export function UserPercentile() { { value: '50', text: I18LABELS.percentile50thMedian, - dropdownDisplay: I18LABELS.percentile50thMedian, 'data-test-subj': 'p50Percentile', }, { value: '75', text: I18LABELS.percentile75th, - dropdownDisplay: I18LABELS.percentile75th, 'data-test-subj': 'p75Percentile', }, { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index a8c4d67305c98..75df1381d8a1d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -10,6 +10,9 @@ export const I18LABELS = { dataMissing: i18n.translate('xpack.apm.rum.dashboard.dataMissing', { defaultMessage: 'N/A', }), + totalPageLoad: i18n.translate('xpack.apm.rum.dashboard.totalPageLoad', { + defaultMessage: 'Total', + }), backEnd: i18n.translate('xpack.apm.rum.dashboard.backend', { defaultMessage: 'Backend', }), @@ -17,7 +20,7 @@ export const I18LABELS = { defaultMessage: 'Frontend', }), pageViews: i18n.translate('xpack.apm.rum.dashboard.pageViews', { - defaultMessage: 'Page views', + defaultMessage: 'Total page views', }), percPageLoaded: i18n.translate('xpack.apm.rum.dashboard.pagesLoaded.label', { defaultMessage: 'Pages loaded', @@ -34,6 +37,9 @@ export const I18LABELS = { defaultMessage: 'Page load duration', } ), + pageLoad: i18n.translate('xpack.apm.rum.dashboard.pageLoad.label', { + defaultMessage: 'Page load', + }), pageLoadDistribution: i18n.translate( 'xpack.apm.rum.dashboard.pageLoadDistribution.label', { @@ -79,8 +85,11 @@ export const I18LABELS = { defaultMessage: 'Operating system', } ), - userExperienceMetrics: i18n.translate('xpack.apm.rum.userExperienceMetrics', { - defaultMessage: 'User experience metrics', + metrics: i18n.translate('xpack.apm.ux.metrics', { + defaultMessage: 'Metrics', + }), + median: i18n.translate('xpack.apm.ux.median', { + defaultMessage: 'median', }), avgPageLoadDuration: i18n.translate( 'xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration', @@ -153,6 +162,21 @@ export const I18LABELS = { noData: i18n.translate('xpack.apm.ux.visitorBreakdown.noData', { defaultMessage: 'No data.', }), + // Helper tooltips + totalPageLoadTooltip: i18n.translate( + 'xpack.apm.rum.dashboard.tooltips.totalPageLoad', + { + defaultMessage: 'Total represents the full page load duration', + } + ), + frontEndTooltip: i18n.translate('xpack.apm.rum.dashboard.tooltips.frontEnd', { + defaultMessage: + 'Frontend time represents the total page load duration minus the backend time', + }), + backEndTooltip: i18n.translate('xpack.apm.rum.dashboard.tooltips.backEnd', { + defaultMessage: + 'Backend time represents time to first byte (TTFB), which is when the first response packet is received after the request has been made', + }), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.tsx new file mode 100644 index 0000000000000..5522cad5690bc --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/utils/test_helper.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render as testLibRender } from '@testing-library/react'; +import { CoreStart } from 'kibana/public'; +import { of } from 'rxjs'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; +import { MemoryHistory } from 'history'; +import { EuiThemeProvider } from '../../../../../../observability/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { UrlParamsProvider } from '../../../../context/UrlParamsContext'; + +export const core = ({ + http: { + basePath: { + prepend: jest.fn(), + }, + }, + uiSettings: { + get: (key: string) => true, + get$: (key: string) => of(true), + }, +} as unknown) as CoreStart; + +export const render = ( + component: React.ReactNode, + options: { customHistory: MemoryHistory } +) => { + const history = options?.customHistory ?? createMemoryHistory(); + + history.location.key = 'TestKeyForTesting'; + + return testLibRender( + + + + {component} + + + + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index cbb6d9a8fbe41..d51e4a2dd3d7c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -8,6 +8,7 @@ import { EuiTabs } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; +import { enableServiceOverview } from '../../../../common/ui_settings_keys'; import { useAgentName } from '../../../hooks/useAgentName'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { EuiTabLink } from '../../shared/EuiTabLink'; @@ -15,21 +16,41 @@ import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink'; import { MetricOverviewLink } from '../../shared/Links/apm/MetricOverviewLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { ServiceNodeOverviewLink } from '../../shared/Links/apm/ServiceNodeOverviewLink'; +import { ServiceOverviewLink } from '../../shared/Links/apm/service_overview_link'; import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; import { ErrorGroupOverview } from '../ErrorGroupOverview'; import { ServiceMap } from '../ServiceMap'; import { ServiceMetrics } from '../ServiceMetrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; +import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../TransactionOverview'; interface Props { serviceName: string; - tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map'; + tab: + | 'errors' + | 'metrics' + | 'nodes' + | 'overview' + | 'service-map' + | 'transactions'; } export function ServiceDetailTabs({ serviceName, tab }: Props) { const { agentName } = useAgentName(); - const { serviceMapEnabled } = useApmPluginContext().config; + const { uiSettings } = useApmPluginContext().core; + + const overviewTab = { + link: ( + + {i18n.translate('xpack.apm.serviceDetails.overviewTabLabel', { + defaultMessage: 'Overview', + })} + + ), + render: () => , + name: 'overview', + }; const transactionsTab = { link: ( @@ -57,7 +78,23 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { name: 'errors', }; - const tabs = [transactionsTab, errorsTab]; + const serviceMapTab = { + link: ( + + {i18n.translate('xpack.apm.home.serviceMapTabLabel', { + defaultMessage: 'Service Map', + })} + + ), + render: () => , + name: 'service-map', + }; + + const tabs = [transactionsTab, errorsTab, serviceMapTab]; + + if (uiSettings.get(enableServiceOverview)) { + tabs.unshift(overviewTab); + } if (isJavaAgentName(agentName)) { const nodesListTab = { @@ -89,22 +126,6 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { tabs.push(metricsTab); } - const serviceMapTab = { - link: ( - - {i18n.translate('xpack.apm.home.serviceMapTabLabel', { - defaultMessage: 'Service Map', - })} - - ), - render: () => , - name: 'service-map', - }; - - if (serviceMapEnabled) { - tabs.push(serviceMapTab); - } - const selectedTab = tabs.find((serviceTab) => serviceTab.name === tab); return ( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index d167b6a9a0565..752f9b7fda243 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -10,6 +10,7 @@ import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, isActivePlatinumLicense, + SERVICE_MAP_TIMEOUT_ERROR, } from '../../../../common/service_map'; import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; @@ -22,6 +23,7 @@ import { Cytoscape } from './Cytoscape'; import { getCytoscapeDivStyle } from './cytoscape_options'; import { EmptyBanner } from './EmptyBanner'; import { EmptyPrompt } from './empty_prompt'; +import { TimeoutPrompt } from './timeout_prompt'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; @@ -61,7 +63,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { const license = useLicense(); const { urlParams } = useUrlParams(); - const { data = { elements: [] }, status } = useFetcher(() => { + const { data = { elements: [] }, status, error } = useFetcher(() => { // When we don't have a license or a valid license, don't make the request. if (!license || !isActivePlatinumLicense(license)) { return; @@ -109,6 +111,20 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { ); } + if ( + status === FETCH_STATUS.FAILURE && + error && + 'body' in error && + error.body.statusCode === 500 && + error.body.message === SERVICE_MAP_TIMEOUT_ERROR + ) { + return ( + + + + ); + } + return (
+ {i18n.translate('xpack.apm.serviceMap.timeoutPromptTitle', { + defaultMessage: 'Service map timeout', + })} + + } + body={ +

+ {i18n.translate('xpack.apm.serviceMap.timeoutPromptDescription', { + defaultMessage: `Timed out while fetching data for service map. Limit the scope by selecting a smaller time range, or use configuration setting '{configName}' with a reduced value.`, + values: { + configName: isGlobalServiceMap + ? 'xpack.apm.serviceMapFingerprintGlobalBucketSize' + : 'xpack.apm.serviceMapFingerprintBucketSize', + }, + })} +

+ } + actions={} + /> + ); +} + +function ApmSettingsDocLink() { + return ( + + {i18n.translate('xpack.apm.serviceMap.timeoutPrompt.docsLink', { + defaultMessage: 'Learn more about APM settings in the docs', + })} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 8e32c55da9161..dfc78028c3596 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { EuiButton, EuiFlexGroup, @@ -37,6 +36,14 @@ export function AgentConfigurations() { return ( <> + +

+ {i18n.translate('xpack.apm.agentConfig.titleText', { + defaultMessage: 'Agent remote configuration', + })} +

+
+ @@ -44,7 +51,7 @@ export function AgentConfigurations() {

{i18n.translate( 'xpack.apm.agentConfig.configurationsPanelTitle', - { defaultMessage: 'Agent remote configuration' } + { defaultMessage: 'Configurations' } )}

diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx index 68e75d595363d..53794ca9965ff 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx @@ -24,11 +24,11 @@ describe('ApmIndices', () => { ); expect(getByText('Indices')).toMatchInlineSnapshot(` -

Indices -

+ `); expect(spy).toHaveBeenCalledTimes(2); diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index 145fb9683cb61..fac947b3ec68e 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -163,23 +163,24 @@ export function ApmIndices() { }; return ( - - - - -

- {i18n.translate('xpack.apm.settings.apmIndices.title', { - defaultMessage: 'Indices', - })} -

-
- - -

- {i18n.translate('xpack.apm.settings.apmIndices.description', { - defaultMessage: `The APM UI uses index patterns to query your APM indices. If you've customized the index names that APM Server writes events to, you may need to update these patterns for the APM UI to work. Settings here take precedence over those set in kibana.yml.`, - })} -

+ <> + +

+ {i18n.translate('xpack.apm.settings.apmIndices.title', { + defaultMessage: 'Indices', + })} +

+
+ + + {i18n.translate('xpack.apm.settings.apmIndices.description', { + defaultMessage: `The APM UI uses index patterns to query your APM indices. If you've customized the index names that APM Server writes events to, you may need to update these patterns for the APM UI to work. Settings here take precedence over those set in kibana.yml.`, + })} + + + + + {APM_INDEX_LABELS.map(({ configurationName, label }) => { const matchedConfiguration = data.find( @@ -239,11 +240,10 @@ export function ApmIndices() { -
-
-
- - -
+
+
+ +
+ ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx index 2e860ebe22c0f..56b3eaf425af7 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export function CreateCustomLinkButton({ onClick }: { onClick: () => void }) { return ( - + {i18n.translate( 'xpack.apm.settings.customizeUI.customLink.createCustomLink', { defaultMessage: 'Create custom link' } diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx index 22d8749d78834..2017aa42e1c5a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui'; + +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; @@ -11,28 +12,14 @@ export function Title() { return ( - + -

+

{i18n.translate('xpack.apm.settings.customizeUI.customLink', { defaultMessage: 'Custom Links', })} -

-
- - - +
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 45a7fa2a118f2..a7d7cf40ba849 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -4,19 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; +import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher'; import { useLicense } from '../../../../../hooks/useLicense'; -import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; +import { LicensePrompt } from '../../../../shared/LicensePrompt'; +import { CreateCustomLinkButton } from './CreateCustomLinkButton'; import { CustomLinkFlyout } from './CustomLinkFlyout'; import { CustomLinkTable } from './CustomLinkTable'; import { EmptyPrompt } from './EmptyPrompt'; import { Title } from './Title'; -import { CreateCustomLinkButton } from './CreateCustomLinkButton'; -import { LicensePrompt } from '../../../../shared/LicensePrompt'; export function CustomLinkOverview() { const license = useLicense(); @@ -82,8 +89,13 @@ export function CustomLinkOverview() {
)}
- - + + + {i18n.translate('xpack.apm.settings.customizeUI.customLink.info', { + defaultMessage: + 'These links will be shown in the Actions context menu for transactions.', + })} + {hasValidLicense ? ( showEmptyPrompt ? ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 6e95df0dddd84..137dcfcdbb4f0 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -80,7 +80,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { - + {i18n.translate( 'xpack.apm.settings.anomalyDetection.jobList.addEnvironments', { 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 new file mode 100644 index 0000000000000..81f23b6427508 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import { useTrackPageview } from '../../../../../observability/public'; +import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink'; +import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; +import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; + +const rowHeight = 310; +const latencyChartRowHeight = 230; + +const Row = styled(EuiFlexItem)` + height: ${rowHeight}px; +`; + +const LatencyChartRow = styled(EuiFlexItem)` + height: ${latencyChartRowHeight}px; +`; + +const TableLinkFlexItem = styled(EuiFlexItem)` + & > a { + text-align: right; + } +`; + +interface ServiceOverviewProps { + serviceName: string; +} + +export function ServiceOverview({ serviceName }: ServiceOverviewProps) { + useTrackPageview({ app: 'apm', path: 'service_overview' }); + useTrackPageview({ app: 'apm', path: 'service_overview', delay: 15000 }); + + return ( + + + + + Search bar + + + Comparison picker + + + Date picker + + + + + + +

+ {i18n.translate('xpack.apm.serviceOverview.latencyChartTitle', { + defaultMessage: 'Latency', + })} +

+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.trafficChartTitle', + { + defaultMessage: 'Traffic', + } + )} +

+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableTitle', + { + defaultMessage: 'Transactions', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.apm.serviceOverview.transactionsTableLinkText', + { + defaultMessage: 'View transactions', + } + )} + + +
+
+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.errorRateChartTitle', + { + defaultMessage: 'Error rate', + } + )} +

+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.errorsTableTitle', + { + defaultMessage: 'Errors', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.apm.serviceOverview.errorsTableLinkText', + { + defaultMessage: 'View errors', + } + )} + + +
+
+
+
+
+ + + + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.averageDurationBySpanTypeChartTitle', + { + defaultMessage: 'Average duration by span type', + } + )} +

+
+
+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableTitle', + { + defaultMessage: 'Dependencies', + } + )} +

+
+
+ + + {i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableLinkText', + { + defaultMessage: 'View service map', + } + )} + + +
+
+
+
+
+ + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.instancesLatencyDistributionChartTitle', + { + defaultMessage: 'Instances latency distribution', + } + )} +

+
+
+
+ + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.instancesTableTitle', + { + defaultMessage: 'Instances', + } + )} +

+
+
+
+
+
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx new file mode 100644 index 0000000000000..4e2063930a9c9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; +import { ServiceOverview } from './'; + +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} + +describe('ServiceOverview', () => { + it('renders', () => { + expect(() => + render(, { + wrapper: Wrapper, + }) + ).not.toThrowError(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx new file mode 100644 index 0000000000000..5d7859e7362c7 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_overview_link.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { APMLink, APMLinkExtendProps } from './APMLink'; + +interface ServiceOverviewLinkProps extends APMLinkExtendProps { + serviceName: string; +} + +export function ServiceOverviewLink({ + serviceName, + ...rest +}: ServiceOverviewLinkProps) { + return ; +} diff --git a/x-pack/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.tsx index 5d65424844c5a..6add0e8a2b480 100644 --- a/x-pack/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/useFetcher.tsx @@ -21,7 +21,7 @@ export enum FETCH_STATUS { export interface FetcherResult { data?: Data; status: FETCH_STATUS; - error?: Error; + error?: IHttpFetchError; } // fetcher functions can return undefined OR a promise. Previously we had a more simple type diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index d6fdb5f52291c..0adfb99e7164e 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -1,8 +1,8 @@ # Documentation for APM UI developers -### Setup local environment +## Local environment setup -#### Kibana +### Kibana ``` git clone git@github.com:elastic/kibana.git @@ -11,15 +11,15 @@ yarn kbn bootstrap yarn start --no-base-path ``` -#### APM Server, Elasticsearch and data +### APM Server, Elasticsearch and data To access an elasticsearch instance that has live data you have two options: -##### A. Connect to Elasticsearch on Cloud (internal devs only) +#### A. Connect to Elasticsearch on Cloud (internal devs only) Find the credentials for the cluster [here](https://github.com/elastic/apm-dev/blob/master/docs/credentials/apm-ui-clusters.md#apmelstcco) -##### B. Start Elastic Stack and APM data generators +#### B. Start Elastic Stack and APM data generators ``` git clone git@github.com:elastic/apm-integration-testing.git @@ -29,6 +29,8 @@ cd apm-integration-testing/ _Docker Compose is required_ +## Testing + ### E2E (Cypress) tests ```sh @@ -109,23 +111,23 @@ The API tests for "trial" are located in `x-pack/test/apm_api_integration/trial/ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) -### Linting +## Linting _Note: Run the following commands from `kibana/`._ -#### Prettier +### Prettier ``` yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write ``` -#### ESLint +### ESLint ``` yarn eslint ./x-pack/plugins/apm --fix ``` -### Setup default APM users +## Setup default APM users APM behaves differently depending on which the role and permissions a logged in user has. For testing purposes APM uses 3 custom users: @@ -144,20 +146,35 @@ node x-pack/plugins/apm/scripts/setup-kibana-security.js --role-suffix Advanced Settings > Observability. + +## Further resources - [Cypress integration tests](./e2e/README.md) - [VSCode setup instructions](./dev_docs/vscode_setup.md) diff --git a/x-pack/plugins/apm/scripts/precommit.js b/x-pack/plugins/apm/scripts/precommit.js index 87da3c1db8b28..ec38ec88f5503 100644 --- a/x-pack/plugins/apm/scripts/precommit.js +++ b/x-pack/plugins/apm/scripts/precommit.js @@ -25,7 +25,7 @@ const tasks = new Listr( [ resolve(__dirname, './jest.js'), '--reporters', - resolve(__dirname, './node_modules/jest-silent-reporter'), + resolve(__dirname, '../../../../node_modules/jest-silent-reporter'), '--collect-coverage', 'false', ], diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index 1fafa08082443..b89c46f6e3fc5 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -9,31 +9,35 @@ Object { }, "body": Object { "aggs": Object { - "backEnd": Object { - "percentiles": Object { - "field": "transaction.marks.agent.timeToFirstByte", - "hdr": Object { - "number_of_significant_value_digits": 3, + "hasFetchStartField": Object { + "aggs": Object { + "backEnd": Object { + "percentiles": Object { + "field": "transaction.marks.agent.timeToFirstByte", + "hdr": Object { + "number_of_significant_value_digits": 3, + }, + "percents": Array [ + 50, + ], + }, }, - "percents": Array [ - 50, - ], - }, - }, - "domInteractive": Object { - "percentiles": Object { - "field": "transaction.marks.agent.domInteractive", - "hdr": Object { - "number_of_significant_value_digits": 3, + "totalPageLoadDuration": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 3, + }, + "percents": Array [ + 50, + ], + }, }, - "percents": Array [ - 50, - ], }, - }, - "pageViews": Object { - "value_count": Object { - "field": "transaction.duration.us", + "filter": Object { + "exists": Object { + "field": "transaction.marks.navigationTiming.fetchStart", + }, }, }, }, @@ -54,11 +58,6 @@ Object { "transaction.type": "page-load", }, }, - Object { - "exists": Object { - "field": "transaction.marks.navigationTiming.fetchStart", - }, - }, Object { "term": Object { "service.environment": "test", @@ -68,6 +67,7 @@ Object { }, }, "size": 0, + "track_total_hits": true, }, } `; @@ -139,11 +139,6 @@ Object { "agent.name": "rum-js", }, }, - Object { - "term": Object { - "transaction.type": "page-load", - }, - }, Object { "term": Object { "service.language.name": "javascript", @@ -545,11 +540,6 @@ Object { "transaction.type": "page-load", }, }, - Object { - "exists": Object { - "field": "transaction.marks.navigationTiming.fetchStart", - }, - }, Object { "term": Object { "service.environment": "test", @@ -590,6 +580,13 @@ Object { ], }, }, + "coreVitalPages": Object { + "filter": Object { + "exists": Object { + "field": "transaction.experience", + }, + }, + }, "fcp": Object { "percentiles": Object { "field": "transaction.marks.agent.firstContentfulPaint", @@ -670,11 +667,6 @@ Object { "service.environment": "test", }, }, - Object { - "term": Object { - "user_agent.name": "Chrome", - }, - }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index 6d596246d6af9..6685a60f84f05 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TRANSACTION_DURATION } from '../../../common/elasticsearch_fieldnames'; import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { - TRANSACTION_DOM_INTERACTIVE, TRANSACTION_TIME_TO_FIRST_BYTE, + TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; export async function getClientMetrics({ @@ -25,32 +24,36 @@ export async function getClientMetrics({ const projection = getRumPageLoadTransactionsProjection({ setup, urlQuery, + checkFetchStartFieldExists: false, }); const params = mergeProjection(projection, { body: { size: 0, + track_total_hits: true, aggs: { - pageViews: { - value_count: { - field: TRANSACTION_DURATION, + hasFetchStartField: { + filter: { + exists: { field: 'transaction.marks.navigationTiming.fetchStart' }, }, - }, - backEnd: { - percentiles: { - field: TRANSACTION_TIME_TO_FIRST_BYTE, - percents: [percentile], - hdr: { - number_of_significant_value_digits: 3, + aggs: { + totalPageLoadDuration: { + percentiles: { + field: TRANSACTION_DURATION, + percents: [percentile], + hdr: { + number_of_significant_value_digits: 3, + }, + }, }, - }, - }, - domInteractive: { - percentiles: { - field: TRANSACTION_DOM_INTERACTIVE, - percents: [percentile], - hdr: { - number_of_significant_value_digits: 3, + backEnd: { + percentiles: { + field: TRANSACTION_TIME_TO_FIRST_BYTE, + percents: [percentile], + hdr: { + number_of_significant_value_digits: 3, + }, + }, }, }, }, @@ -59,18 +62,21 @@ export async function getClientMetrics({ }); const { apmEventClient } = setup; - const response = await apmEventClient.search(params); - const { backEnd, domInteractive, pageViews } = response.aggregations!; + const { + hasFetchStartField: { backEnd, totalPageLoadDuration }, + } = response.aggregations!; const pkey = percentile.toFixed(1); - // Divide by 1000 to convert ms into seconds + const totalPageLoadDurationValue = totalPageLoadDuration.values[pkey] ?? 0; + const totalPageLoadDurationValueMs = totalPageLoadDurationValue / 1000; // Microseconds to milliseconds + const backendValue = backEnd.values[pkey] ?? 0; + return { - pageViews, - backEnd: { value: backEnd.values[pkey] || 0 }, - frontEnd: { - value: (domInteractive.values[pkey] || 0) - (backEnd.values[pkey] || 0), - }, + pageViews: { value: response.hits.total.value ?? 0 }, + totalPageLoadDuration: { value: totalPageLoadDurationValueMs }, + backEnd: { value: backendValue }, + frontEnd: { value: totalPageLoadDurationValueMs - backendValue }, }; } diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index c1a602c33feae..f483acba4b934 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -20,6 +20,7 @@ export async function getPageViewTrends({ const projection = getRumPageLoadTransactionsProjection({ setup, urlQuery, + checkFetchStartFieldExists: false, }); let breakdownItem: BreakdownItem | null = null; if (breakdowns) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index c5baf0b529eb4..76a718bbb2a02 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -12,7 +12,6 @@ import { FCP_FIELD, FID_FIELD, LCP_FIELD, - USER_AGENT_NAME, TBT_FIELD, } from '../../../common/elasticsearch_fieldnames'; @@ -35,17 +34,17 @@ export async function getWebCoreVitals({ size: 0, query: { bool: { - filter: [ - ...projection.body.query.bool.filter, - { - term: { - [USER_AGENT_NAME]: 'Chrome', - }, - }, - ], + filter: [...projection.body.query.bool.filter], }, }, aggs: { + coreVitalPages: { + filter: { + exists: { + field: 'transaction.experience', + }, + }, + }, lcp: { percentiles: { field: LCP_FIELD, @@ -104,13 +103,22 @@ export async function getWebCoreVitals({ const { apmEventClient } = setup; const response = await apmEventClient.search(params); - const { lcp, cls, fid, tbt, fcp, lcpRanks, fidRanks, clsRanks } = - response.aggregations ?? {}; + const { + lcp, + cls, + fid, + tbt, + fcp, + lcpRanks, + fidRanks, + clsRanks, + coreVitalPages, + } = response.aggregations ?? {}; const getRanksPercentages = ( - ranks: Array<{ key: number; value: number }> + ranks?: Array<{ key: number; value: number }> ) => { - const ranksVal = ranks.map(({ value }) => value?.toFixed(0) ?? 0); + const ranksVal = ranks?.map(({ value }) => value?.toFixed(0) ?? 0) ?? []; return [ Number(ranksVal?.[0]), Number(ranksVal?.[1]) - Number(ranksVal?.[0]), @@ -118,23 +126,26 @@ export async function getWebCoreVitals({ ]; }; - const defaultRanks = [ - { value: 0, key: 0 }, - { value: 0, key: 0 }, - ]; + const defaultRanks = [100, 0, 0]; const pkey = percentile.toFixed(1); - // Divide by 1000 to convert ms into seconds return { - cls: String(cls?.values[pkey]?.toFixed(2) || 0), - fid: fid?.values[pkey] ?? 0, - lcp: lcp?.values[pkey] ?? 0, + coreVitalPages: coreVitalPages?.doc_count ?? 0, + cls: cls?.values[pkey]?.toFixed(3) || null, + fid: fid?.values[pkey], + lcp: lcp?.values[pkey], tbt: tbt?.values[pkey] ?? 0, - fcp: fcp?.values[pkey] ?? 0, + fcp: fcp?.values[pkey], - lcpRanks: getRanksPercentages(lcpRanks?.values ?? defaultRanks), - fidRanks: getRanksPercentages(fidRanks?.values ?? defaultRanks), - clsRanks: getRanksPercentages(clsRanks?.values ?? defaultRanks), + lcpRanks: lcp?.values[pkey] + ? getRanksPercentages(lcpRanks?.values) + : defaultRanks, + fidRanks: fid?.values[pkey] + ? getRanksPercentages(fidRanks?.values) + : defaultRanks, + clsRanks: cls?.values[pkey] + ? getRanksPercentages(clsRanks?.values) + : defaultRanks, }; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index dfc4e02c25a7f..524b9bfdc7891 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq, take, sortBy } from 'lodash'; +import Boom from 'boom'; import { ProcessorEvent } from '../../../common/processor_event'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { rangeFilter } from '../../../common/utils/range_filter'; @@ -15,6 +16,7 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, } from '../../../common/elasticsearch_fieldnames'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { SERVICE_MAP_TIMEOUT_ERROR } from '../../../common/service_map'; const MAX_TRACES_TO_INSPECT = 1000; @@ -122,26 +124,30 @@ export async function getTraceSampleIds({ }, }; - const tracesSampleResponse = await apmEventClient.search(params); + try { + const tracesSampleResponse = await apmEventClient.search(params); + // make sure at least one trace per composite/connection bucket + // is queried + const traceIdsWithPriority = + tracesSampleResponse.aggregations?.connections.buckets.flatMap((bucket) => + bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({ + traceId: sampleDocBucket.key as string, + priority: index, + })) + ) || []; - // make sure at least one trace per composite/connection bucket - // is queried - const traceIdsWithPriority = - tracesSampleResponse.aggregations?.connections.buckets.flatMap((bucket) => - bucket.sample.trace_ids.buckets.map((sampleDocBucket, index) => ({ - traceId: sampleDocBucket.key as string, - priority: index, - })) - ) || []; + const traceIds = take( + uniq( + sortBy(traceIdsWithPriority, 'priority').map(({ traceId }) => traceId) + ), + MAX_TRACES_TO_INSPECT + ); - const traceIds = take( - uniq( - sortBy(traceIdsWithPriority, 'priority').map(({ traceId }) => traceId) - ), - MAX_TRACES_TO_INSPECT - ); - - return { - traceIds, - }; + return { traceIds }; + } catch (error) { + if ('displayName' in error && error.displayName === 'RequestTimeout') { + throw Boom.internal(SERVICE_MAP_TIMEOUT_ERROR); + } + throw error; + } } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index b417f8689b229..d3341b6c1b163 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { i18n } from '@kbn/i18n'; import { combineLatest, Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; @@ -36,6 +37,7 @@ import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_cust import { createApmApi } from './routes/create_apm_api'; import { apmIndices, apmTelemetry } from './saved_objects'; import { createElasticCloudInstructions } from './tutorial/elastic_cloud'; +import { uiSettings } from './ui_settings'; export interface APMPluginSetup { config$: Observable; @@ -75,6 +77,8 @@ export class APMPlugin implements Plugin { core.savedObjects.registerType(apmIndices); core.savedObjects.registerType(apmTelemetry); + core.uiSettings.register(uiSettings); + if (plugins.actions && plugins.alerts) { registerApmAlerts({ alerts: plugins.alerts, diff --git a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index 530ca36b5dc9f..9c33da2278694 100644 --- a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -17,9 +17,11 @@ import { TRANSACTION_PAGE_LOAD } from '../../common/transaction_types'; export function getRumPageLoadTransactionsProjection({ setup, urlQuery, + checkFetchStartFieldExists = true, }: { setup: Setup & SetupTimeRange; urlQuery?: string; + checkFetchStartFieldExists?: boolean; }) { const { start, end, esFilter } = setup; @@ -27,13 +29,17 @@ export function getRumPageLoadTransactionsProjection({ filter: [ { range: rangeFilter(start, end) }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, - { - // Adding this filter to cater for some inconsistent rum data - // not available on aggregated transactions - exists: { - field: 'transaction.marks.navigationTiming.fetchStart', - }, - }, + ...(checkFetchStartFieldExists + ? [ + { + // Adding this filter to cater for some inconsistent rum data + // not available on aggregated transactions + exists: { + field: 'transaction.marks.navigationTiming.fetchStart', + }, + }, + ] + : []), ...(urlQuery ? [ { @@ -74,7 +80,6 @@ export function getRumErrorsProjection({ filter: [ { range: rangeFilter(start, end) }, { term: { [AGENT_NAME]: 'rum-js' } }, - { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { term: { [SERVICE_LANGUAGE_NAME]: 'javascript', diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts new file mode 100644 index 0000000000000..fe5b11d89d716 --- /dev/null +++ b/x-pack/plugins/apm/server/ui_settings.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from '../../../../src/core/types'; +import { + enableCorrelations, + enableServiceOverview, +} from '../common/ui_settings_keys'; + +/** + * uiSettings definitions for APM. + */ +export const uiSettings: Record> = { + [enableCorrelations]: { + category: ['Observability'], + name: i18n.translate('xpack.apm.enableCorrelationsExperimentName', { + defaultMessage: 'APM Correlations', + }), + value: false, + description: i18n.translate( + 'xpack.apm.enableCorrelationsExperimentDescription', + { + defaultMessage: + 'Enable the experimental correlations UI and API endpoint in APM.', + } + ), + schema: schema.boolean(), + }, + [enableServiceOverview]: { + category: ['Observability'], + name: i18n.translate('xpack.apm.enableServiceOverviewExperimentName', { + defaultMessage: 'APM Service overview', + }), + value: false, + description: i18n.translate( + 'xpack.apm.enableServiceOverviewExperimentDescription', + { + defaultMessage: 'Enable the Overview tab for services in APM.', + } + ), + schema: schema.boolean(), + }, +}; diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index 8b3163e44915a..a869c1e1186fa 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -145,6 +145,15 @@ export interface AggregationOptionsByType { >; keyed?: boolean; } & AggregationSourceOptions; + range: { + field: string; + ranges: Array< + | { key?: string; from: string | number } + | { key?: string; to: string | number } + | { key?: string; from: string | number; to: string | number } + >; + keyed?: boolean; + }; auto_date_histogram: { buckets: number; } & AggregationSourceOptions; @@ -324,6 +333,18 @@ interface AggregationResponsePart< ? Record : { buckets: DateRangeBucket[] }; }; + range: { + buckets: TAggregationOptionsMap extends { range: { keyed: true } } + ? Record< + string, + DateRangeBucket & + SubAggregationResponseOf + > + : Array< + DateRangeBucket & + SubAggregationResponseOf + >; + }; auto_date_histogram: { buckets: Array< DateHistogramBucket & diff --git a/x-pack/plugins/canvas/public/feature_catalogue_entry.ts b/x-pack/plugins/canvas/public/feature_catalogue_entry.ts index ccc642177780d..2b6d2f105454b 100644 --- a/x-pack/plugins/canvas/public/feature_catalogue_entry.ts +++ b/x-pack/plugins/canvas/public/feature_catalogue_entry.ts @@ -11,7 +11,7 @@ export const featureCatalogueEntry = { id: 'canvas', title: 'Canvas', subtitle: i18n.translate('xpack.canvas.featureCatalogue.canvasSubtitle', { - defaultMessage: 'Design pixel-perfect reports.', + defaultMessage: 'Design pixel-perfect presentations.', }), description: i18n.translate('xpack.canvas.appDescription', { defaultMessage: 'Showcase your data in a pixel-perfect way.', diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts new file mode 100644 index 0000000000000..f253dd9f4feb4 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -0,0 +1,335 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConnectorTypes, CasePostRequest } from '../../../common/api'; + +import { + createMockSavedObjectsRepository, + mockCaseConfigure, + mockCases, +} from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; + +describe('create', () => { + beforeEach(async () => { + jest.restoreAllMocks(); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); + }); + + describe('happy path', () => { + test('it creates the case correctly', async () => { + const postCase = { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + tags: ['defacement'], + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + } as CasePostRequest; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseConfigureSavedObject: mockCaseConfigure, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.create({ theCase: postCase }); + + expect(res).toEqual({ + id: 'mock-it', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { full_name: 'Awesome D00d', email: 'd00d@awesome.com', username: 'awesome' }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + + expect( + caseClient.services.userActionService.postUserActions.mock.calls[0][0].actions + ).toEqual([ + { + attributes: { + action: 'create', + action_at: '2019-11-25T21:54:48.952Z', + action_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + action_field: ['description', 'status', 'tags', 'title', 'connector'], + new_value: + '{"description":"This is a brand new case of a bad meanie defacing data","title":"Super Bad Security Issue","tags":["defacement"],"connector":{"id":"123","name":"Jira","type":".jira","fields":{"issueType":"Task","priority":"High","parent":null}}}', + old_value: null, + }, + references: [ + { + id: 'mock-it', + name: 'associated-cases', + type: 'cases', + }, + ], + }, + ]); + }); + + test('it creates the case without connector in the configuration', async () => { + const postCase = { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + tags: ['defacement'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.create({ theCase: postCase }); + + expect(res).toEqual({ + id: 'mock-it', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { full_name: 'Awesome D00d', email: 'd00d@awesome.com', username: 'awesome' }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + }); + + test('Allow user to create case without authentication', async () => { + const postCase = { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + tags: ['defacement'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); + const res = await caseClient.client.create({ theCase: postCase }); + + expect(res).toEqual({ + id: 'mock-it', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + email: null, + full_name: null, + username: null, + }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'open', + tags: ['defacement'], + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + }); + }); + + describe('unhappy path', () => { + test('it throws when missing title', async () => { + expect.assertions(1); + const postCase = { + description: 'This is a brand new case of a bad meanie defacing data', + tags: ['defacement'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing description', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + tags: ['defacement'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing tags', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + description: 'This is a brand new case of a bad meanie defacing data', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing connector ', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + description: 'This is a brand new case of a bad meanie defacing data', + tags: ['defacement'], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when connector missing the right fields', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + description: 'This is a brand new case of a bad meanie defacing data', + tags: ['defacement'], + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: {}, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws if you passing status for a new case', async () => { + expect.assertions(1); + const postCase = { + title: 'a title', + description: 'This is a brand new case of a bad meanie defacing data', + tags: ['defacement'], + status: 'closed', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client.create({ theCase: postCase }).catch((e) => expect(e).not.toBeNull()); + }); + + it(`Returns an error if postNewCase throws`, async () => { + const postCase = { + description: 'Throw an error', + title: 'Super Bad Security Issue', + tags: ['error'], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }; + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + + caseClient.client.create({ theCase: postCase }).catch((e) => expect(e).not.toBeNull()); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/cases/create.ts b/x-pack/plugins/case/server/client/cases/create.ts new file mode 100644 index 0000000000000..3379099419a75 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/create.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; + +import { + CasePostRequestRt, + throwErrors, + excess, + CaseResponseRt, + CaseResponse, +} from '../../../common/api'; +import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; +import { + getConnectorFromConfiguration, + transformCaseConnectorToEsConnector, +} from '../../routes/api/cases/helpers'; + +import { CaseClientCreate, CaseClientFactoryArguments } from '../types'; + +export const create = ({ + savedObjectsClient, + caseService, + caseConfigureService, + userActionService, + request, +}: CaseClientFactoryArguments) => async ({ theCase }: CaseClientCreate): Promise => { + const query = pipe( + excess(CasePostRequestRt).decode(theCase), + fold(throwErrors(Boom.badRequest), identity) + ); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = await caseService.getUser({ request }); + const createdDate = new Date().toISOString(); + const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); + const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); + + const newCase = await caseService.postNewCase({ + client: savedObjectsClient, + attributes: transformNewCase({ + createdDate, + newCase: query, + username, + full_name, + email, + connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), + }), + }); + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCaseUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: newCase.id, + fields: ['description', 'status', 'tags', 'title', 'connector'], + newValue: JSON.stringify(query), + }), + ], + }); + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: newCase, + }) + ); +}; diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts new file mode 100644 index 0000000000000..62d897999c11a --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -0,0 +1,383 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConnectorTypes, CasesPatchRequest } from '../../../common/api'; +import { + createMockSavedObjectsRepository, + mockCaseNoConnectorId, + mockCases, +} from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; + +describe('update', () => { + beforeEach(async () => { + jest.restoreAllMocks(); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); + }); + + describe('happy path', () => { + test('it closes the case correctly', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + status: 'closed' as const, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ cases: patchCases }); + + expect(res).toEqual([ + { + closed_at: '2019-11-25T21:54:48.952Z', + closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + comments: [], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' }, + description: 'This is a brand new case of a bad meanie defacing data', + id: 'mock-id-1', + external_service: null, + status: 'closed', + tags: ['defacement'], + title: 'Super Bad Security Issue', + totalComment: 0, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }, + ]); + + expect( + caseClient.services.userActionService.postUserActions.mock.calls[0][0].actions + ).toEqual([ + { + attributes: { + action: 'update', + action_at: '2019-11-25T21:54:48.952Z', + action_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + action_field: ['status'], + new_value: 'closed', + old_value: 'open', + }, + references: [ + { + id: 'mock-id-1', + name: 'associated-cases', + type: 'cases', + }, + ], + }, + ]); + }); + + test('it opens the case correctly', async () => { + const patchCases = { + cases: [ + { + id: 'mock-id-1', + status: 'open' as const, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: [ + { ...mockCases[0], attributes: { ...mockCases[0].attributes, status: 'closed' } }, + ...mockCases.slice(1), + ], + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ cases: patchCases }); + + expect(res).toEqual([ + { + closed_at: null, + closed_by: null, + comments: [], + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' }, + description: 'This is a brand new case of a bad meanie defacing data', + id: 'mock-id-1', + external_service: null, + status: 'open', + tags: ['defacement'], + title: 'Super Bad Security Issue', + totalComment: 0, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }, + ]); + }); + + test('it updates a case without a connector.id', async () => { + const patchCases = { + cases: [ + { + id: 'mock-no-connector_id', + status: 'closed' as const, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: [mockCaseNoConnectorId], + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ cases: patchCases }); + + expect(res).toEqual([ + { + id: 'mock-no-connector_id', + comments: [], + totalComment: 0, + closed_at: '2019-11-25T21:54:48.952Z', + closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { full_name: 'elastic', email: 'testemail@elastic.co', username: 'elastic' }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + title: 'Super Bad Security Issue', + status: 'closed', + tags: ['defacement'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }, + ]); + }); + + test('it updates the connector correctly', async () => { + const patchCases = ({ + cases: [ + { + id: 'mock-id-3', + connector: { + id: '456', + name: 'My connector 2', + type: ConnectorTypes.jira, + fields: { issueType: 'Bug', priority: 'Low', parent: null }, + }, + version: 'WzUsMV0=', + }, + ], + } as unknown) as CasesPatchRequest; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.update({ cases: patchCases }); + + expect(res).toEqual([ + { + id: 'mock-id-3', + comments: [], + totalComment: 0, + closed_at: null, + closed_by: null, + connector: { + id: '456', + name: 'My connector 2', + type: ConnectorTypes.jira, + fields: { issueType: 'Bug', priority: 'Low', parent: null }, + }, + created_at: '2019-11-25T22:32:17.947Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'Oh no, a bad meanie going LOLBins all over the place!', + external_service: null, + title: 'Another bad one', + status: 'open', + tags: ['LOLBins'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'Awesome D00d', + email: 'd00d@awesome.com', + username: 'awesome', + }, + version: 'WzE3LDFd', + }, + ]); + }); + }); + + describe('unhappy path', () => { + test('it throws when missing id', async () => { + expect.assertions(1); + const patchCases = { + cases: [ + { + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + version: 'WzUsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when missing version', async () => { + expect.assertions(1); + const patchCases = { + cases: [ + { + id: 'mock-id-3', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => expect(e).not.toBeNull()); + }); + + test('it throws when fields are identical', async () => { + expect.assertions(1); + const patchCases = { + cases: [ + { + id: 'mock-id-1', + status: 'open' as const, + version: 'WzAsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .update({ cases: patchCases }) + .catch((e) => + expect(e.message).toBe('All update fields are identical to current version.') + ); + }); + + test('it throws when case does not exist', async () => { + const patchCases = { + cases: [ + { + id: 'not-exists', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + version: 'WzUsMV0=', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .update({ cases: patchCases }) + .catch((e) => + expect(e.message).toBe( + 'These cases not-exists do not exist. Please check you have the correct ids.' + ) + ); + }); + + test('it throws when cases conflicts', async () => { + expect.assertions(1); + const patchCases = { + cases: [ + { + id: 'mock-id-1', + version: 'WzAsMV1=', + title: 'Super Bad Security Issue', + }, + ], + }; + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .update({ cases: patchCases }) + .catch((e) => + expect(e.message).toBe( + 'These cases mock-id-1 has been updated. Please refresh before saving additional updates.' + ) + ); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/cases/update.ts b/x-pack/plugins/case/server/client/cases/update.ts new file mode 100644 index 0000000000000..424f51ee40f08 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { flattenCaseSavedObject } from '../../routes/api/utils'; + +import { + throwErrors, + excess, + CasesResponseRt, + CasesPatchRequestRt, + ESCasePatchRequest, + CasePatchRequest, + CasesResponse, +} from '../../../common/api'; +import { buildCaseUserActions } from '../../services/user_actions/helpers'; +import { + getCaseToUpdate, + transformCaseConnectorToEsConnector, +} from '../../routes/api/cases/helpers'; + +import { CaseClientUpdate, CaseClientFactoryArguments } from '../types'; + +export const update = ({ + savedObjectsClient, + caseService, + userActionService, + request, +}: CaseClientFactoryArguments) => async ({ cases }: CaseClientUpdate): Promise => { + const query = pipe( + excess(CasesPatchRequestRt).decode(cases), + fold(throwErrors(Boom.badRequest), identity) + ); + + const myCases = await caseService.getCases({ + client: savedObjectsClient, + caseIds: query.cases.map((q) => q.id), + }); + + let nonExistingCases: CasePatchRequest[] = []; + const conflictedCases = query.cases.filter((q) => { + const myCase = myCases.saved_objects.find((c) => c.id === q.id); + + if (myCase && myCase.error) { + nonExistingCases = [...nonExistingCases, q]; + return false; + } + return myCase == null || myCase?.version !== q.version; + }); + + if (nonExistingCases.length > 0) { + throw Boom.notFound( + `These cases ${nonExistingCases + .map((c) => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } + + if (conflictedCases.length > 0) { + throw Boom.conflict( + `These cases ${conflictedCases + .map((c) => c.id) + .join(', ')} has been updated. Please refresh before saving additional updates.` + ); + } + + const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { + const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); + const { connector, ...thisCase } = updateCase; + return currentCase != null + ? getCaseToUpdate(currentCase.attributes, { + ...thisCase, + ...(connector != null + ? { connector: transformCaseConnectorToEsConnector(connector) } + : {}), + }) + : { id: thisCase.id, version: thisCase.version }; + }); + + const updateFilterCases = updateCases.filter((updateCase) => { + const { id, version, ...updateCaseAttributes } = updateCase; + return Object.keys(updateCaseAttributes).length > 0; + }); + + if (updateFilterCases.length > 0) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = await caseService.getUser({ request }); + const updatedDt = new Date().toISOString(); + const updatedCases = await caseService.patchCases({ + client: savedObjectsClient, + cases: updateFilterCases.map((thisCase) => { + const { id: caseId, version, ...updateCaseAttributes } = thisCase; + let closedInfo = {}; + if (updateCaseAttributes.status && updateCaseAttributes.status === 'closed') { + closedInfo = { + closed_at: updatedDt, + closed_by: { email, full_name, username }, + }; + } else if (updateCaseAttributes.status && updateCaseAttributes.status === 'open') { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + caseId, + updatedAttributes: { + ...updateCaseAttributes, + ...closedInfo, + updated_at: updatedDt, + updated_by: { email, full_name, username }, + }, + version, + }; + }), + }); + + const returnUpdatedCase = myCases.saved_objects + .filter((myCase) => + updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) + ) + .map((myCase) => { + const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); + return flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + version: updatedCase?.version ?? myCase.version, + }, + }); + }); + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: buildCaseUserActions({ + originalCases: myCases.saved_objects, + updatedCases: updatedCases.saved_objects, + actionDate: updatedDt, + actionBy: { email, full_name, username }, + }), + }); + + return CasesResponseRt.encode(returnUpdatedCase); + } + throw Boom.notAcceptable('All update fields are identical to current version.'); +}; diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts new file mode 100644 index 0000000000000..8a316740e41e0 --- /dev/null +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockSavedObjectsRepository, + mockCaseComments, + mockCases, +} from '../../routes/api/__fixtures__'; +import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; + +describe('addComment', () => { + beforeEach(async () => { + jest.restoreAllMocks(); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2020-10-23T21:54:48.952Z'), + })); + }); + + describe('happy path', () => { + test('it adds a comment correctly', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }); + + expect(res.id).toEqual('mock-id-1'); + expect(res.totalComment).toEqual(res.comments!.length); + expect(res.comments![res.comments!.length - 1]).toEqual({ + comment: 'Wow, good luck catching that bad meanie!', + created_at: '2020-10-23T21:54:48.952Z', + created_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + id: 'mock-comment', + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + }); + + test('it updates the case correctly after adding a comment', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + const res = await caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }); + + expect(res.updated_at).toEqual('2020-10-23T21:54:48.952Z'); + expect(res.updated_by).toEqual({ + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }); + }); + + test('it creates a user action', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + await caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }); + + expect( + caseClient.services.userActionService.postUserActions.mock.calls[0][0].actions + ).toEqual([ + { + attributes: { + action: 'create', + action_at: '2020-10-23T21:54:48.952Z', + action_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + action_field: ['comment'], + new_value: 'Wow, good luck catching that bad meanie!', + old_value: null, + }, + references: [ + { + id: 'mock-id-1', + name: 'associated-cases', + type: 'cases', + }, + { + id: 'mock-comment', + name: 'associated-cases-comments', + type: 'cases-comments', + }, + ], + }, + ]); + }); + + test('it allow user to create comments without authentications', async () => { + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); + const res = await caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }); + + expect(res.id).toEqual('mock-id-1'); + expect(res.comments![res.comments!.length - 1]).toEqual({ + comment: 'Wow, good luck catching that bad meanie!', + created_at: '2020-10-23T21:54:48.952Z', + created_by: { + email: null, + full_name: null, + username: null, + }, + id: 'mock-comment', + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); + }); + }); + + describe('unhappy path', () => { + test('it throws when missing comment', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .addComment({ + caseId: 'mock-id-1', + // @ts-expect-error + comment: {}, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); + + test('it throws when the case does not exists', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .addComment({ + caseId: 'not-exists', + comment: { comment: 'Wow, good luck catching that bad meanie!' }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(404); + }); + }); + + test('it throws when postNewCase throws', async () => { + expect.assertions(3); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); + caseClient.client + .addComment({ + caseId: 'mock-id-1', + comment: { comment: 'Throw an error' }, + }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }); + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts new file mode 100644 index 0000000000000..765eb2c873765 --- /dev/null +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { flattenCaseSavedObject, transformNewComment } from '../../routes/api/utils'; + +import { + throwErrors, + excess, + CaseResponseRt, + CommentRequestRt, + CaseResponse, +} from '../../../common/api'; +import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; + +import { CaseClientAddComment, CaseClientFactoryArguments } from '../types'; +import { CASE_SAVED_OBJECT } from '../../saved_object_types'; + +export const addComment = ({ + savedObjectsClient, + caseService, + userActionService, + request, +}: CaseClientFactoryArguments) => async ({ + caseId, + comment, +}: CaseClientAddComment): Promise => { + const query = pipe( + excess(CommentRequestRt).decode(comment), + fold(throwErrors(Boom.badRequest), identity) + ); + + const myCase = await caseService.getCase({ + client: savedObjectsClient, + caseId, + }); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = await caseService.getUser({ request }); + const createdDate = new Date().toISOString(); + + const [newComment, updatedCase] = await Promise.all([ + caseService.postNewComment({ + client: savedObjectsClient, + attributes: transformNewComment({ + createdDate, + ...query, + username, + full_name, + email, + }), + references: [ + { + type: CASE_SAVED_OBJECT, + name: `associated-${CASE_SAVED_OBJECT}`, + id: myCase.id, + }, + ], + }), + caseService.patchCase({ + client: savedObjectsClient, + caseId, + updatedAttributes: { + updated_at: createdDate, + updated_by: { username, full_name, email }, + }, + version: myCase.version, + }), + ]); + + const totalCommentsFindByCases = await caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId, + options: { + fields: [], + page: 1, + perPage: 1, + }, + }); + + const [comments] = await Promise.all([ + caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId, + options: { + fields: [], + page: 1, + perPage: totalCommentsFindByCases.total, + }, + }), + userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCommentUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: myCase.id, + commentId: newComment.id, + fields: ['comment'], + newValue: query.comment, + }), + ], + }), + ]); + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase.attributes }, + version: updatedCase.version ?? myCase.version, + references: myCase.references, + }, + comments: comments.saved_objects, + }) + ); +}; diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts new file mode 100644 index 0000000000000..1ecdc8ea96dea --- /dev/null +++ b/x-pack/plugins/case/server/client/index.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { createCaseClient } from '.'; +import { + createCaseServiceMock, + createConfigureServiceMock, + createUserActionServiceMock, +} from '../services/mocks'; + +import { create } from './cases/create'; +import { update } from './cases/update'; +import { addComment } from './comments/add'; + +jest.mock('./cases/create'); +jest.mock('./cases/update'); +jest.mock('./comments/add'); + +const caseService = createCaseServiceMock(); +const caseConfigureService = createConfigureServiceMock(); +const userActionService = createUserActionServiceMock(); +const savedObjectsClient = savedObjectsClientMock.create(); +const request = {} as KibanaRequest; + +const createMock = create as jest.Mock; +const updateMock = update as jest.Mock; +const addCommentMock = addComment as jest.Mock; + +describe('createCaseClient()', () => { + test('it creates the client correctly', async () => { + createCaseClient({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }); + + expect(createMock).toHaveBeenCalledWith({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }); + + expect(updateMock).toHaveBeenCalledWith({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }); + + expect(addCommentMock).toHaveBeenCalledWith({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }); + }); +}); diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts new file mode 100644 index 0000000000000..75e9e3c4cfebc --- /dev/null +++ b/x-pack/plugins/case/server/client/index.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CaseClientFactoryArguments, CaseClient } from './types'; +import { create } from './cases/create'; +import { update } from './cases/update'; +import { addComment } from './comments/add'; + +export { CaseClient } from './types'; + +export const createCaseClient = ({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, +}: CaseClientFactoryArguments): CaseClient => { + return { + create: create({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }), + update: update({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }), + addComment: addComment({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + }), + }; +}; diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts new file mode 100644 index 0000000000000..243dd884f9ef6 --- /dev/null +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { CaseService, CaseConfigureService, CaseUserActionServiceSetup } from '../services'; +import { CaseClient } from './types'; +import { authenticationMock } from '../routes/api/__fixtures__'; +import { createCaseClient } from '.'; + +export type CaseClientMock = jest.Mocked; +export const createCaseClientMock = (): CaseClientMock => ({ + create: jest.fn(), + update: jest.fn(), + addComment: jest.fn(), +}); + +export const createCaseClientWithMockSavedObjectsClient = async ( + savedObjectsClient: any, + badAuth: boolean = false +): Promise<{ + client: CaseClient; + services: { userActionService: jest.Mocked }; +}> => { + const log = loggingSystemMock.create().get('case'); + const request = {} as KibanaRequest; + + const caseServicePlugin = new CaseService(log); + const caseConfigureServicePlugin = new CaseConfigureService(log); + + const caseService = await caseServicePlugin.setup({ + authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), + }); + const caseConfigureService = await caseConfigureServicePlugin.setup(); + const userActionService = { + postUserActions: jest.fn(), + getUserActions: jest.fn(), + }; + + return { + client: createCaseClient({ + savedObjectsClient, + request, + caseService, + caseConfigureService, + userActionService, + }), + services: { userActionService }, + }; +}; diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts new file mode 100644 index 0000000000000..8db7d8a5747d7 --- /dev/null +++ b/x-pack/plugins/case/server/client/types.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest, SavedObjectsClientContract } from '../../../../../src/core/server'; +import { + CasePostRequest, + CasesPatchRequest, + CommentRequest, + CaseResponse, + CasesResponse, +} from '../../common/api'; +import { + CaseConfigureServiceSetup, + CaseServiceSetup, + CaseUserActionServiceSetup, +} from '../services'; + +export interface CaseClientCreate { + theCase: CasePostRequest; +} + +export interface CaseClientUpdate { + cases: CasesPatchRequest; +} + +export interface CaseClientAddComment { + caseId: string; + comment: CommentRequest; +} + +export interface CaseClientFactoryArguments { + savedObjectsClient: SavedObjectsClientContract; + request: KibanaRequest; + caseConfigureService: CaseConfigureServiceSetup; + caseService: CaseServiceSetup; + userActionService: CaseUserActionServiceSetup; +} + +export interface CaseClient { + create: (args: CaseClientCreate) => Promise; + update: (args: CaseClientUpdate) => Promise; + addComment: (args: CaseClientAddComment) => Promise; +} diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 9cf045da3e700..5398f8ed0ae83 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,10 +5,17 @@ */ import { first, map } from 'rxjs/operators'; -import { Logger, PluginInitializerContext } from 'kibana/server'; -import { CoreSetup } from 'src/core/server'; +import { + IContextProvider, + KibanaRequest, + Logger, + PluginInitializerContext, + RequestHandler, +} from 'kibana/server'; +import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; +import { APP_ID } from '../common/constants'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; @@ -18,7 +25,15 @@ import { caseCommentSavedObjectType, caseUserActionSavedObjectType, } from './saved_object_types'; -import { CaseConfigureService, CaseService, CaseUserActionService } from './services'; +import { + CaseConfigureService, + CaseConfigureServiceSetup, + CaseService, + CaseServiceSetup, + CaseUserActionService, + CaseUserActionServiceSetup, +} from './services'; +import { createCaseClient } from './client'; function createConfig$(context: PluginInitializerContext) { return context.config.create().pipe(map((config) => config)); @@ -30,6 +45,9 @@ export interface PluginsSetup { export class CasePlugin { private readonly log: Logger; + private caseService?: CaseServiceSetup; + private caseConfigureService?: CaseConfigureServiceSetup; + private userActionService?: CaseUserActionServiceSetup; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get(); @@ -47,36 +65,83 @@ export class CasePlugin { core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseUserActionSavedObjectType); - const caseServicePlugin = new CaseService(this.log); - const caseConfigureServicePlugin = new CaseConfigureService(this.log); - const userActionServicePlugin = new CaseUserActionService(this.log); - this.log.debug( `Setting up Case Workflow with core contract [${Object.keys( core )}] and plugins [${Object.keys(plugins)}]` ); - const caseService = await caseServicePlugin.setup({ + this.caseService = await new CaseService(this.log).setup({ authentication: plugins.security != null ? plugins.security.authc : null, }); - const caseConfigureService = await caseConfigureServicePlugin.setup(); - const userActionService = await userActionServicePlugin.setup(); + this.caseConfigureService = await new CaseConfigureService(this.log).setup(); + this.userActionService = await new CaseUserActionService(this.log).setup(); + + core.http.registerRouteHandlerContext( + APP_ID, + this.createRouteHandlerContext({ + core, + caseService: this.caseService, + caseConfigureService: this.caseConfigureService, + userActionService: this.userActionService, + }) + ); const router = core.http.createRouter(); initCaseApi({ - caseConfigureService, - caseService, - userActionService, + caseService: this.caseService, + caseConfigureService: this.caseConfigureService, + userActionService: this.userActionService, router, }); } - public start() { + public async start(core: CoreStart) { this.log.debug(`Starting Case Workflow`); + + const getCaseClientWithRequest = async (request: KibanaRequest) => { + return createCaseClient({ + savedObjectsClient: core.savedObjects.getScopedClient(request), + request, + caseService: this.caseService!, + caseConfigureService: this.caseConfigureService!, + userActionService: this.userActionService!, + }); + }; + + return { + getCaseClientWithRequest, + }; } public stop() { this.log.debug(`Stopping Case Workflow`); } + + private createRouteHandlerContext = ({ + core, + caseService, + caseConfigureService, + userActionService, + }: { + core: CoreSetup; + caseService: CaseServiceSetup; + caseConfigureService: CaseConfigureServiceSetup; + userActionService: CaseUserActionServiceSetup; + }): IContextProvider, typeof APP_ID> => { + return async (context, request) => { + const [{ savedObjects }] = await core.getStartServices(); + return { + getCaseClient: () => { + return createCaseClient({ + savedObjectsClient: savedObjects.getScopedClient(request), + caseService, + caseConfigureService, + userActionService, + request, + }); + }, + }; + }; + }; } diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index c2df91148a53a..8bbd419e6315b 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -39,7 +39,15 @@ export const createMockSavedObjectsRepository = ({ } const result = caseSavedObject.filter((s) => s.id === id); if (!result.length) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + return { + id, + type, + error: { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [cases/not-exist] not found', + }, + }; } return result[0]; }), diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 265970b1abdec..e7ea381da9955 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject } from 'kibana/server'; +import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import { ESCasesConfigureAttributes, CommentAttributes, @@ -325,3 +325,12 @@ export const mockCaseConfigure: Array> = version: 'WzYsMV0=', }, ]; + +export const mockCaseConfigureFind: Array> = [ + { + page: 1, + per_page: 5, + total: mockCaseConfigure.length, + saved_objects: [{ ...mockCaseConfigure[0], score: 0 }], + }, +]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index d947ffbaf181d..67890599fa417 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -4,13 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from 'src/core/server'; +import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { createCaseClient } from '../../../client'; +import { CaseService, CaseConfigureService } from '../../../services'; import { getActions } from '../__mocks__/request_responses'; +import { authenticationMock } from '../__fixtures__'; -export const createRouteContext = (client: any) => { +export const createRouteContext = async (client: any, badAuth = false) => { const actionsMock = actionsClientMock.create(); actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + const log = loggingSystemMock.create().get('case'); + + const caseServicePlugin = new CaseService(log); + const caseConfigureServicePlugin = new CaseConfigureService(log); + + const caseService = await caseServicePlugin.setup({ + authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), + }); + const caseConfigureService = await caseConfigureServicePlugin.setup(); + const caseClient = createCaseClient({ + savedObjectsClient: client, + request: {} as KibanaRequest, + caseService, + caseConfigureService, + userActionService: { + postUserActions: jest.fn(), + getUserActions: jest.fn(), + }, + }); return ({ core: { @@ -19,5 +42,8 @@ export const createRouteContext = (client: any) => { }, }, actions: { getActionsClient: () => actionsMock }, + case: { + getCaseClient: () => caseClient, + }, } as unknown) as RequestHandlerContext; }; diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts index 67cb998409570..986ad3a54496f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts @@ -32,7 +32,7 @@ describe('DELETE comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -52,7 +52,7 @@ describe('DELETE comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts index 24a03b217ab7c..23f64151a78d7 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts @@ -32,7 +32,7 @@ describe('GET comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -57,7 +57,7 @@ describe('GET comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseCommentSavedObject: mockCaseComments, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts index 04473e302e468..400e8ca404ca5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts @@ -35,7 +35,7 @@ describe('PATCH comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -63,7 +63,7 @@ describe('PATCH comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -87,7 +87,7 @@ describe('PATCH comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index 9006470f36f36..acc23815e3a39 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -26,6 +26,7 @@ describe('POST comment', () => { toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), })); }); + it(`Posts a new comment`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -38,7 +39,7 @@ describe('POST comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -51,6 +52,7 @@ describe('POST comment', () => { 'mock-comment' ); }); + it(`Returns an error if the case does not exist`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -63,7 +65,7 @@ describe('POST comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -74,6 +76,7 @@ describe('POST comment', () => { expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); }); + it(`Returns an error if postNewCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: CASE_COMMENTS_URL, @@ -86,7 +89,7 @@ describe('POST comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -97,6 +100,7 @@ describe('POST comment', () => { expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); + it(`Allow user to create comments without authentications`, async () => { routeHandler = await createRoute(initPostCommentApi, 'post', true); @@ -111,11 +115,12 @@ describe('POST comment', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, - }) + }), + true ); const response = await routeHandler(theContext, request, kibanaResponseFactory); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 3c5b72eba5d13..08d442bccf2cb 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -5,24 +5,12 @@ */ import { schema } from '@kbn/config-schema'; -import Boom from 'boom'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; - -import { CaseResponseRt, CommentRequestRt, excess, throwErrors } from '../../../../../common/api'; -import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; -import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; -import { escapeHatch, transformNewComment, wrapError, flattenCaseSavedObject } from '../../utils'; +import { escapeHatch, wrapError } from '../../utils'; import { RouteDeps } from '../../types'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CommentRequest } from '../../../../../common/api'; -export function initPostCommentApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPostCommentApi({ router }: RouteDeps) { router.post( { path: CASE_COMMENTS_URL, @@ -34,101 +22,17 @@ export function initPostCommentApi({ }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const caseId = request.params.case_id; - const query = pipe( - excess(CommentRequestRt).decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const myCase = await caseService.getCase({ - client, - caseId, - }); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request, response }); - const createdDate = new Date().toISOString(); - - const [newComment, updatedCase] = await Promise.all([ - caseService.postNewComment({ - client, - attributes: transformNewComment({ - createdDate, - ...query, - username, - full_name, - email, - }), - references: [ - { - type: CASE_SAVED_OBJECT, - name: `associated-${CASE_SAVED_OBJECT}`, - id: myCase.id, - }, - ], - }), - caseService.patchCase({ - client, - caseId, - updatedAttributes: { - updated_at: createdDate, - updated_by: { username, full_name, email }, - }, - version: myCase.version, - }), - ]); - - const totalCommentsFindByCases = await caseService.getAllCaseComments({ - client, - caseId, - options: { - fields: [], - page: 1, - perPage: 1, - }, - }); + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const [comments] = await Promise.all([ - caseService.getAllCaseComments({ - client, - caseId, - options: { - fields: [], - page: 1, - perPage: totalCommentsFindByCases.total, - }, - }), - userActionService.postUserActions({ - client, - actions: [ - buildCommentUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: myCase.id, - commentId: newComment.id, - fields: ['comment'], - newValue: query.comment, - }), - ], - }), - ]); + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; + const comment = request.body as CommentRequest; + try { return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase.attributes }, - version: updatedCase.version ?? myCase.version, - references: myCase.references, - }, - comments: comments.saved_objects, - }) - ), + body: await caseClient.addComment({ caseId, comment }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts index 45ce19fca9d20..cc4f208758369 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -29,7 +29,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -49,7 +49,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }], }) @@ -87,7 +87,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [], }) @@ -105,7 +105,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], }) diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index ee4dcc8e81b95..2eab4ac756361 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -29,7 +29,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -106,7 +106,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts index 8fcb769225d44..261cd3e6b0884 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts @@ -39,7 +39,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -72,7 +72,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -110,7 +110,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -141,7 +141,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [], }) @@ -163,7 +163,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -190,7 +190,7 @@ describe('PATCH configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts index 27df19d8f823a..7ef3bdb4a700a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -37,7 +37,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -72,7 +72,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -112,7 +112,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -137,7 +137,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -162,7 +162,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -187,7 +187,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -212,7 +212,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -234,7 +234,7 @@ describe('POST configuration', () => { caseConfigureSavedObject: mockCaseConfigure, }); - const context = createRouteContext(savedObjectRepository); + const context = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -253,7 +253,7 @@ describe('POST configuration', () => { caseConfigureSavedObject: [], }); - const context = createRouteContext(savedObjectRepository); + const context = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -275,7 +275,7 @@ describe('POST configuration', () => { ], }); - const context = createRouteContext(savedObjectRepository); + const context = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -291,7 +291,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], }) @@ -309,7 +309,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-delete' }], }) @@ -334,7 +334,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -360,7 +360,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -385,7 +385,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) @@ -406,7 +406,7 @@ describe('POST configuration', () => { }, }); - const context = createRouteContext( + const context = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts index e655339e05eb1..3970534140cd8 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts @@ -32,7 +32,7 @@ describe('DELETE case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -51,7 +51,7 @@ describe('DELETE case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -70,7 +70,7 @@ describe('DELETE case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, caseCommentSavedObject: mockCaseComments, @@ -89,7 +89,7 @@ describe('DELETE case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, caseCommentSavedObject: mockCasesErrorTriggerData, diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts index df27551d2c922..b2ba8b2fcb33a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts @@ -29,7 +29,7 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -46,7 +46,7 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -63,7 +63,7 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) @@ -80,7 +80,7 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], caseConfigureSavedObject: mockCaseConfigure, diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts index 224da4464e1c2..01de9abac16af 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts @@ -39,7 +39,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -70,7 +70,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -94,7 +94,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -119,7 +119,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, }) @@ -142,7 +142,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) @@ -171,7 +171,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], caseConfigureSavedObject: mockCaseConfigure, @@ -201,7 +201,7 @@ describe('GET case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts index c0d19edcad91f..ea69ee77c5802 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts @@ -43,7 +43,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -93,7 +93,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, @@ -144,7 +144,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) @@ -170,7 +170,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -201,7 +201,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -232,7 +232,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -257,7 +257,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -283,7 +283,7 @@ describe('PATCH cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts index 79e2e99731546..873671a909801 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts @@ -4,31 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; - -import { - CasesPatchRequestRt, - CasesResponseRt, - CasePatchRequest, - excess, - throwErrors, - ESCasePatchRequest, -} from '../../../../common/api'; -import { escapeHatch, wrapError, flattenCaseSavedObject } from '../utils'; +import { escapeHatch, wrapError } from '../utils'; import { RouteDeps } from '../types'; -import { getCaseToUpdate, transformCaseConnectorToEsConnector } from './helpers'; -import { buildCaseUserActions } from '../../../services/user_actions/helpers'; import { CASES_URL } from '../../../../common/constants'; +import { CasesPatchRequest } from '../../../../common/api'; -export function initPatchCasesApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPatchCasesApi({ router }: RouteDeps) { router.patch( { path: CASES_URL, @@ -37,126 +18,17 @@ export function initPatchCasesApi({ }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const query = pipe( - excess(CasesPatchRequestRt).decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const myCases = await caseService.getCases({ - client, - caseIds: query.cases.map((q) => q.id), - }); - - let nonExistingCases: CasePatchRequest[] = []; - const conflictedCases = query.cases.filter((q) => { - const myCase = myCases.saved_objects.find((c) => c.id === q.id); - - if (myCase && myCase.error) { - nonExistingCases = [...nonExistingCases, q]; - return false; - } - return myCase == null || myCase?.version !== q.version; - }); - if (nonExistingCases.length > 0) { - throw Boom.notFound( - `These cases ${nonExistingCases - .map((c) => c.id) - .join(', ')} do not exist. Please check you have the correct ids.` - ); - } - if (conflictedCases.length > 0) { - throw Boom.conflict( - `These cases ${conflictedCases - .map((c) => c.id) - .join(', ')} has been updated. Please refresh before saving additional updates.` - ); - } + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { - const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); - const { connector, ...thisCase } = updateCase; - return currentCase != null - ? getCaseToUpdate(currentCase.attributes, { - ...thisCase, - ...(connector != null - ? { connector: transformCaseConnectorToEsConnector(connector) } - : {}), - }) - : { id: thisCase.id, version: thisCase.version }; - }); + const caseClient = context.case.getCaseClient(); + const cases = request.body as CasesPatchRequest; - const updateFilterCases = updateCases.filter((updateCase) => { - const { id, version, ...updateCaseAttributes } = updateCase; - return Object.keys(updateCaseAttributes).length > 0; + try { + return response.ok({ + body: await caseClient.update({ cases }), }); - - if (updateFilterCases.length > 0) { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request, response }); - const updatedDt = new Date().toISOString(); - const updatedCases = await caseService.patchCases({ - client, - cases: updateFilterCases.map((thisCase) => { - const { id: caseId, version, ...updateCaseAttributes } = thisCase; - let closedInfo = {}; - if (updateCaseAttributes.status && updateCaseAttributes.status === 'closed') { - closedInfo = { - closed_at: updatedDt, - closed_by: { email, full_name, username }, - }; - } else if (updateCaseAttributes.status && updateCaseAttributes.status === 'open') { - closedInfo = { - closed_at: null, - closed_by: null, - }; - } - return { - caseId, - updatedAttributes: { - ...updateCaseAttributes, - ...closedInfo, - updated_at: updatedDt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); - - const returnUpdatedCase = myCases.saved_objects - .filter((myCase) => - updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) - ) - .map((myCase) => { - const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); - return flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - version: updatedCase?.version ?? myCase.version, - }, - }); - }); - - await userActionService.postUserActions({ - client, - actions: buildCaseUserActions({ - originalCases: myCases.saved_objects, - updatedCases: updatedCases.saved_objects, - actionDate: updatedDt, - actionBy: { email, full_name, username }, - }), - }); - - return response.ok({ - body: CasesResponseRt.encode(returnUpdatedCase), - }); - } - throw Boom.notAcceptable('All update fields are identical to current version.'); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index be1ed4166ab7b..1e1b19baa1c47 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -45,7 +45,7 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -80,7 +80,7 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, @@ -110,7 +110,7 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -132,7 +132,7 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -162,11 +162,12 @@ describe('POST cases', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, - }) + }), + true ); const response = await routeHandler(theContext, request, kibanaResponseFactory); diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts index 5d8113b685741..663d502d548d5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts @@ -4,25 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; +import { wrapError, escapeHatch } from '../utils'; -import { flattenCaseSavedObject, transformNewCase, wrapError, escapeHatch } from '../utils'; - -import { CasePostRequestRt, throwErrors, excess, CaseResponseRt } from '../../../../common/api'; -import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; -import { getConnectorFromConfiguration, transformCaseConnectorToEsConnector } from './helpers'; +import { CasePostRequest } from '../../../../common/api'; -export function initPostCaseApi({ - caseService, - caseConfigureService, - router, - userActionService, -}: RouteDeps) { +export function initPostCaseApi({ router }: RouteDeps) { router.post( { path: CASES_URL, @@ -31,53 +19,15 @@ export function initPostCaseApi({ }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const query = pipe( - excess(CasePostRequestRt).decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request, response }); - const createdDate = new Date().toISOString(); - const myCaseConfigure = await caseConfigureService.find({ client }); - const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); - - const newCase = await caseService.postNewCase({ - client, - attributes: transformNewCase({ - createdDate, - newCase: query, - username, - full_name, - email, - connector: transformCaseConnectorToEsConnector( - query.connector ?? caseConfigureConnector - ), - }), - }); - - await userActionService.postUserActions({ - client, - actions: [ - buildCaseUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector'], - newValue: JSON.stringify(query), - }), - ], - }); + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + const caseClient = context.case.getCaseClient(); + const theCase = request.body as CasePostRequest; + try { return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: newCase, - }) - ), + body: await caseClient.create({ theCase }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts index c68b4b0c91735..eee59a974b37b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts @@ -44,7 +44,7 @@ describe('Push case', () => { body: caseExternalServiceRequestBody, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) @@ -66,7 +66,7 @@ describe('Push case', () => { body: caseExternalServiceRequestBody, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: [ @@ -97,7 +97,7 @@ describe('Push case', () => { }, }); - const theContext = createRouteContext( + const theContext = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 2202bda2be087..90066bf29ae66 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -121,7 +121,7 @@ export const flattenCaseSavedObjects = ( export const flattenCaseSavedObject = ({ savedObject, comments = [], - totalComment = 0, + totalComment = comments.length, }: { savedObject: SavedObject; comments?: Array>; diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 3db83331a0ab9..cab8cb499c3fa 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -96,7 +96,7 @@ interface PatchComments extends ClientArgs { interface GetUserArgs { request: KibanaRequest; - response: KibanaResponseFactory; + response?: KibanaResponseFactory; } interface CaseServiceDeps { diff --git a/x-pack/plugins/case/server/services/mocks.ts b/x-pack/plugins/case/server/services/mocks.ts new file mode 100644 index 0000000000000..287f80a60ab07 --- /dev/null +++ b/x-pack/plugins/case/server/services/mocks.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CaseConfigureServiceSetup, CaseServiceSetup, CaseUserActionServiceSetup } from '.'; + +export type CaseServiceMock = jest.Mocked; +export type CaseConfigureServiceMock = jest.Mocked; +export type CaseUserActionServiceMock = jest.Mocked; + +export const createCaseServiceMock = (): CaseServiceMock => ({ + deleteCase: jest.fn(), + deleteComment: jest.fn(), + findCases: jest.fn(), + getAllCaseComments: jest.fn(), + getCase: jest.fn(), + getCases: jest.fn(), + getComment: jest.fn(), + getTags: jest.fn(), + getReporters: jest.fn(), + getUser: jest.fn(), + postNewCase: jest.fn(), + postNewComment: jest.fn(), + patchCase: jest.fn(), + patchCases: jest.fn(), + patchComment: jest.fn(), + patchComments: jest.fn(), +}); + +export const createConfigureServiceMock = (): CaseConfigureServiceMock => ({ + delete: jest.fn(), + get: jest.fn(), + find: jest.fn(), + patch: jest.fn(), + post: jest.fn(), +}); + +export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({ + getUserActions: jest.fn(), + postUserActions: jest.fn(), +}); diff --git a/x-pack/plugins/case/server/types.ts b/x-pack/plugins/case/server/types.ts new file mode 100644 index 0000000000000..b95060ef30452 --- /dev/null +++ b/x-pack/plugins/case/server/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CaseClient } from './client'; + +export interface CaseRequestContext { + getCaseClient: () => CaseClient; +} + +declare module 'src/core/server' { + interface RequestHandlerContext { + case?: CaseRequestContext; + } +} diff --git a/x-pack/plugins/console_extensions/README.md b/x-pack/plugins/console_extensions/README.md new file mode 100644 index 0000000000000..49d83d2888d6b --- /dev/null +++ b/x-pack/plugins/console_extensions/README.md @@ -0,0 +1,3 @@ +# Console extensions + +This plugin provides autocomplete definitions of licensed APIs to the OSS Console plugin. \ No newline at end of file diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts index d0c597532f6ed..e1d8a2b3671a2 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts @@ -12,8 +12,9 @@ import { } from '../../../../../../../src/plugins/ui_actions/public'; /** - * We know that VALUE_CLICK_TRIGGER and SELECT_RANGE_TRIGGER are also triggering APPLY_FILTER_TRIGGER - * This function appends APPLY_FILTER_TRIGGER to list of triggers if VALUE_CLICK_TRIGGER or SELECT_RANGE_TRIGGER + * We know that VALUE_CLICK_TRIGGER and SELECT_RANGE_TRIGGER are also triggering APPLY_FILTER_TRIGGER. + * This function appends APPLY_FILTER_TRIGGER to the list of triggers if either VALUE_CLICK_TRIGGER + * or SELECT_RANGE_TRIGGER was executed. * * TODO: this probably should be part of uiActions infrastructure, * but dynamic implementation of nested trigger doesn't allow to statically express such relations diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx index d5547ff8097cc..ff54e0812975d 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx @@ -129,7 +129,7 @@ describe('isCompatible', () => { }); }); - test('not compatible if no triggers intersection', async () => { + test('not compatible if no triggers intersect', async () => { await assertNonCompatibility({ actionFactoriesTriggers: [], }); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index a2192808c2d40..a417deb47db53 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -10,9 +10,12 @@ import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/pub import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { isEnhancedEmbeddable, - embeddableEnhancedContextMenuDrilldownGrouping, + embeddableEnhancedDrilldownGrouping, } from '../../../../../../embeddable_enhanced/public'; -import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public'; +import { + CONTEXT_MENU_TRIGGER, + EmbeddableContext, +} from '../../../../../../../../src/plugins/embeddable/public'; import { StartDependencies } from '../../../../plugin'; import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; import { ensureNestedTriggers } from '../drilldown_shared'; @@ -27,7 +30,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType handle.close()} viewMode={'create'} dynamicActionManager={embeddable.enhancements.dynamicActions} - triggers={ensureNestedTriggers(embeddable.supportedTriggers())} + triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} /> ), diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 56ef25005078b..1f0570445a8fc 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -10,12 +10,16 @@ import { reactToUiComponent, toMountPoint, } from '../../../../../../../../src/plugins/kibana_react/public'; -import { EmbeddableContext, ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; +import { + EmbeddableContext, + ViewMode, + CONTEXT_MENU_TRIGGER, +} from '../../../../../../../../src/plugins/embeddable/public'; import { txtDisplayName } from './i18n'; import { MenuItem } from './menu_item'; import { isEnhancedEmbeddable, - embeddableEnhancedContextMenuDrilldownGrouping, + embeddableEnhancedDrilldownGrouping, } from '../../../../../../embeddable_enhanced/public'; import { StartDependencies } from '../../../../plugin'; import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; @@ -31,7 +35,7 @@ export class FlyoutEditDrilldownAction implements ActionByType handle.close()} viewMode={'manage'} dynamicActionManager={embeddable.enhancements.dynamicActions} - triggers={ensureNestedTriggers(embeddable.supportedTriggers())} + triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} /> ), diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts index 67fc1a98ad4d1..4b1e4b34da86a 100644 --- a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts +++ b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse as parseUrl } from 'url'; import { OnPostAuthHandler, OnPostAuthToolkit, @@ -45,7 +46,7 @@ describe('DashboardOnlyModeRequestInterceptor', () => { test('should not redirects for not app/* requests', async () => { const request = ({ url: { - path: 'api/test', + pathname: 'api/test', }, } as unknown) as KibanaRequest; @@ -57,7 +58,7 @@ describe('DashboardOnlyModeRequestInterceptor', () => { test('should not redirects not authenticated users', async () => { const request = ({ url: { - path: '/app/home', + pathname: '/app/home', }, } as unknown) as KibanaRequest; @@ -70,10 +71,9 @@ describe('DashboardOnlyModeRequestInterceptor', () => { function testRedirectToDashboardModeApp(url: string) { describe(`requests to url:"${url}"`, () => { test('redirects to the dashboard_mode app instead', async () => { + const { pathname, search, hash } = parseUrl(url); const request = ({ - url: { - path: url, - }, + url: { pathname, search, hash }, credentials: { roles: [DASHBOARD_ONLY_MODE_ROLE], }, diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts index 4378c818f087c..9978d18142ff5 100644 --- a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts +++ b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts @@ -22,7 +22,7 @@ export const setupDashboardModeRequestInterceptor = ({ getUiSettingsClient, }: DashboardModeRequestInterceptorDependencies) => (async (request, response, toolkit) => { - const path = request.url.path || ''; + const path = request.url.pathname; const isAppRequest = path.startsWith('/app/'); if (!isAppRequest) { diff --git a/x-pack/plugins/data_enhanced/README.md b/x-pack/plugins/data_enhanced/README.md new file mode 100644 index 0000000000000..8f3ae7ac3cd13 --- /dev/null +++ b/x-pack/plugins/data_enhanced/README.md @@ -0,0 +1,25 @@ +# data_enhanced + +The `data_enhanced` plugin is the x-pack counterpart to the OSS `data` plugin. + +It exists to provide Elastic-licensed services, or parts of services, which +enhance existing OSS functionality from `data`. + +Currently the `data_enhanced` plugin doesn't return any APIs which you can +consume directly, however it is possible that you are indirectly relying on the +enhanced functionality that it provides via the OSS `data` plugin. + +Here is the functionality it adds: + +## KQL Autocomplete + +The OSS autocomplete service provides suggestions for field names and values +based on suggestion providers which are registered to the service. This plugin +registers the autocomplete provider for KQL to the OSS service. + +## Async, Rollup, and EQL Search Strategies + +This plugin enhances the OSS search service with an ES search strategy that +uses async search (or rollups) behind the scenes. It also registers an EQL +search strategy. + diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 3187b41a2c55f..f47d2b39a89a9 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { MockedKeys } from '@kbn/utility-types/jest'; import { coreMock } from '../../../../../src/core/public/mocks'; import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx index 85e92d0827daa..807dfeed21d1f 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx @@ -6,7 +6,11 @@ import React from 'react'; import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; -import { ChartActionContext, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { + ChartActionContext, + CONTEXT_MENU_TRIGGER, + IEmbeddable, +} from '../../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; import { SELECT_RANGE_TRIGGER, @@ -34,7 +38,10 @@ interface UrlDrilldownDeps { export type ActionContext = ChartActionContext; export type Config = UrlDrilldownConfig; -export type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER; +export type UrlTrigger = + | typeof CONTEXT_MENU_TRIGGER + | typeof VALUE_CLICK_TRIGGER + | typeof SELECT_RANGE_TRIGGER; export interface ActionFactoryContext extends BaseActionFactoryContext { embeddable?: IEmbeddable; } @@ -58,7 +65,7 @@ export class UrlDrilldown implements Drilldown = ({ diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts index 6989819da2b0b..a93e150deee8f 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts @@ -87,25 +87,25 @@ describe('VALUE_CLICK_TRIGGER', () => { ]) as ValueClickTriggerEventScope; expect(mockEventScope.points.length).toBeGreaterThan(3); expect(mockEventScope.points).toMatchInlineSnapshot(` - Array [ - Object { - "key": "event.points.0.key", - "value": "event.points.0.value", - }, - Object { - "key": "event.points.1.key", - "value": "event.points.1.value", - }, - Object { - "key": "event.points.2.key", - "value": "event.points.2.value", - }, - Object { - "key": "event.points.3.key", - "value": "event.points.3.value", - }, - ] - `); + Array [ + Object { + "key": "event.points.0.key", + "value": "event.points.0.value", + }, + Object { + "key": "event.points.1.key", + "value": "event.points.1.value", + }, + Object { + "key": "event.points.2.key", + "value": "event.points.2.value", + }, + Object { + "key": "event.points.3.key", + "value": "event.points.3.value", + }, + ] + `); }); }); @@ -130,3 +130,12 @@ describe('VALUE_CLICK_TRIGGER', () => { }); }); }); + +describe('CONTEXT_MENU_TRIGGER', () => { + test('getMockEventScope() results in empty scope', () => { + const mockEventScope = getMockEventScope([ + 'CONTEXT_MENU_TRIGGER', + ]) as ValueClickTriggerEventScope; + expect(mockEventScope).toEqual({}); + }); +}); diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts index 0f66cb144c967..234af380689e9 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts @@ -14,11 +14,15 @@ import { IEmbeddable, isRangeSelectTriggerContext, isValueClickTriggerContext, + isContextMenuTriggerContext, RangeSelectContext, ValueClickContext, } from '../../../../../../src/plugins/embeddable/public'; import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown'; -import { SELECT_RANGE_TRIGGER } from '../../../../../../src/plugins/ui_actions/public'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../../src/plugins/ui_actions/public'; type ContextScopeInput = ActionContext | ActionFactoryContext; @@ -101,7 +105,10 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld * URL drilldown event scope, * available as {{event.$}} */ -export type UrlDrilldownEventScope = ValueClickTriggerEventScope | RangeSelectTriggerEventScope; +export type UrlDrilldownEventScope = + | ValueClickTriggerEventScope + | RangeSelectTriggerEventScope + | ContextMenuTriggerEventScope; export type EventScopeInput = ActionContext; export interface ValueClickTriggerEventScope { key?: string; @@ -115,11 +122,15 @@ export interface RangeSelectTriggerEventScope { to?: string | number; } +export type ContextMenuTriggerEventScope = object; + export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEventScope { if (isRangeSelectTriggerContext(eventScopeInput)) { return getEventScopeFromRangeSelectTriggerContext(eventScopeInput); } else if (isValueClickTriggerContext(eventScopeInput)) { return getEventScopeFromValueClickTriggerContext(eventScopeInput); + } else if (isContextMenuTriggerContext(eventScopeInput)) { + return {}; } else { throw new Error("UrlDrilldown [getEventScope] can't build scope from not supported trigger"); } @@ -169,7 +180,9 @@ export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventSco from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago to: new Date().toISOString(), }; - } else { + } + + if (trigger === VALUE_CLICK_TRIGGER) { // number of mock points to generate // should be larger or equal of any possible data points length emitted by VALUE_CLICK_TRIGGER const nPoints = 4; @@ -184,6 +197,8 @@ export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventSco points, }; } + + return {}; } type Primitive = string | number | boolean | null; diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts b/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts index 5ea8928532c28..0aa1c0e6f08ae 100644 --- a/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts +++ b/x-pack/plugins/embeddable_enhanced/public/actions/drilldown_grouping.ts @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; import { UiActionsPresentableGrouping as PresentableGrouping } from '../../../../../src/plugins/ui_actions/public'; -export const contextMenuDrilldownGrouping: PresentableGrouping<{ +export const drilldownGrouping: PresentableGrouping<{ embeddable?: IEmbeddable; }> = [ { id: 'drilldowns', - getDisplayName: () => 'Drilldowns', + getDisplayName: () => + i18n.translate('xpack.embeddableEnhanced.Drilldowns', { + defaultMessage: 'Drilldowns', + }), getIconType: () => 'symlink', order: 25, }, diff --git a/x-pack/plugins/embeddable_enhanced/public/index.ts b/x-pack/plugins/embeddable_enhanced/public/index.ts index a7916685239df..24f8eb623abe0 100644 --- a/x-pack/plugins/embeddable_enhanced/public/index.ts +++ b/x-pack/plugins/embeddable_enhanced/public/index.ts @@ -20,4 +20,4 @@ export function plugin(context: PluginInitializerContext) { export { EnhancedEmbeddable, EnhancedEmbeddableContext } from './types'; export { isEnhancedEmbeddable } from './embeddables'; -export { contextMenuDrilldownGrouping as embeddableEnhancedContextMenuDrilldownGrouping } from './actions'; +export { drilldownGrouping as embeddableEnhancedDrilldownGrouping } from './actions'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts index fb1b6db45e762..44b040d19909c 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ISavedObjectTypeRegistry, KibanaRequest, diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts index 72af8060de827..f68e920e14de9 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { IRouter, Logger } from '../../../../../src/core/server'; import { ConfigType } from '../config'; import { EncryptionKeyRotationService } from '../crypto'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts index 0e5be4e4eee5a..0191c1fbb830d 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { StartServicesAccessor, SavedObject, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx index 567c77792583d..7af3a1d1272e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx @@ -7,10 +7,12 @@ import React from 'react'; import { EuiPage, EuiPageContent } from '@elastic/eui'; +import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { ErrorStatePrompt } from '../../../shared/error_state'; export const ErrorConnecting: React.FC = () => ( + diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts index c3e2aff6551c9..440f540ccc857 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts @@ -20,6 +20,8 @@ describe('Enterprise Search Telemetry Usage Collector', () => { get: () => ({ attributes: { 'ui_viewed.overview': 10, + 'ui_viewed.setup_guide': 5, + 'ui_error.cannot_connect': 1, 'ui_clicked.app_search': 2, 'ui_clicked.workplace_search': 3, }, @@ -53,6 +55,10 @@ describe('Enterprise Search Telemetry Usage Collector', () => { expect(savedObjectsCounts).toEqual({ ui_viewed: { overview: 10, + setup_guide: 5, + }, + ui_error: { + cannot_connect: 1, }, ui_clicked: { app_search: 2, @@ -74,6 +80,10 @@ describe('Enterprise Search Telemetry Usage Collector', () => { expect(savedObjectsCounts).toEqual({ ui_viewed: { overview: 0, + setup_guide: 0, + }, + ui_error: { + cannot_connect: 0, }, ui_clicked: { app_search: 0, diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts index a124a185b9a34..d6bd8bd9305f5 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts @@ -13,6 +13,10 @@ import { getSavedObjectAttributesFromRepo } from '../lib/telemetry'; interface ITelemetry { ui_viewed: { overview: number; + setup_guide: number; + }; + ui_error: { + cannot_connect: number; }; ui_clicked: { app_search: number; @@ -38,6 +42,10 @@ export const registerTelemetryUsageCollector = ( schema: { ui_viewed: { overview: { type: 'long' }, + setup_guide: { type: 'long' }, + }, + ui_error: { + cannot_connect: { type: 'long' }, }, ui_clicked: { app_search: { type: 'long' }, @@ -63,6 +71,10 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log const defaultTelemetrySavedObject: ITelemetry = { ui_viewed: { overview: 0, + setup_guide: 0, + }, + ui_error: { + cannot_connect: 0, }, ui_clicked: { app_search: 0, @@ -78,6 +90,10 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log return { ui_viewed: { overview: get(savedObjectAttributes, 'ui_viewed.overview', 0), + setup_guide: get(savedObjectAttributes, 'ui_viewed.setup_guide', 0), + }, + ui_error: { + cannot_connect: get(savedObjectAttributes, 'ui_error.cannot_connect', 0), }, ui_clicked: { app_search: get(savedObjectAttributes, 'ui_clicked.app_search', 0), diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 2bddc9f1c80bd..5bd15ce411002 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -21,7 +21,6 @@ describe('callEnterpriseSearchConfigAPI', () => { accessCheckTimeoutWarning: 100, }; const mockRequest = { - url: { path: '/app/kibana' }, headers: { authorization: '==someAuth' }, }; const mockDependencies = { diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 8c3e6e11b75c5..fa9f9c36052a1 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -6,6 +6,7 @@ import { reject, isUndefined } from 'lodash'; import { SearchResponse, Client } from 'elasticsearch'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Logger, LegacyClusterClient } from 'src/core/server'; import { IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts index 3273fe847080f..d9846428b9488 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -114,7 +114,7 @@ describe('EventLogStart', () => { ).toEqual(result); expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( - esContext.esNames.alias, + esContext.esNames.indexPattern, undefined, 'saved-object-type', 'saved-object-id', @@ -195,7 +195,7 @@ describe('EventLogStart', () => { ).toEqual(result); expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( - esContext.esNames.alias, + esContext.esNames.indexPattern, undefined, 'saved-object-type', 'saved-object-id', @@ -322,6 +322,12 @@ function FakeRequest(): KibanaRequest { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: () => savedObjectGetter, } as unknown) as KibanaRequest; } diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 32fd99d170026..b7de4acb9428c 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -92,7 +92,7 @@ export class EventLogClient implements IEventLogClient { await this.savedObjectGetter(type, id); return await this.esContext.esAdapter.queryEventsBySavedObject( - this.esContext.esNames.alias, + this.esContext.esNames.indexPattern, namespace, type, id, diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts index 0a5b169e87d4d..db6f4a1ad0f27 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.test.ts @@ -56,6 +56,12 @@ function fakeRequest(): KibanaRequest { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: () => savedObjectsClient, } as unknown) as KibanaRequest; } diff --git a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts index b0ce5605d0e51..0ea5419b113d2 100644 --- a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts @@ -6,6 +6,7 @@ import { identity, merge } from 'lodash'; import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'src/core/server'; +import type { MethodKeysOf } from '@kbn/utility-types'; import { httpServerMock } from 'src/core/server/mocks'; import { IEventLogClient } from '../types'; diff --git a/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts index 6a02d54c87514..076260ab2fe53 100644 --- a/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts @@ -93,6 +93,12 @@ function fakeRequest(): KibanaRequest { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, getSavedObjectsClient: () => savedObjectsClient, } as unknown) as KibanaRequest; } diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.ts index d582dbfdab50c..03773a4d8f616 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { RecursiveReadonly } from '@kbn/utility-types'; +import type { RecursiveReadonly, Writable } from '@kbn/utility-types'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { ElasticsearchFeature, KibanaFeature } from '../common'; diff --git a/x-pack/plugins/file_upload/README.md b/x-pack/plugins/file_upload/README.md new file mode 100644 index 0000000000000..0d4b4da61ccf6 --- /dev/null +++ b/x-pack/plugins/file_upload/README.md @@ -0,0 +1,3 @@ +# File upload + +Backend and core front-end react-components for GeoJson file upload. Only supports the Maps plugin. \ No newline at end of file diff --git a/x-pack/plugins/global_search/common/license_checker.ts b/x-pack/plugins/global_search/common/license_checker.ts index d201b31802b32..7524d0ad52e45 100644 --- a/x-pack/plugins/global_search/common/license_checker.ts +++ b/x-pack/plugins/global_search/common/license_checker.ts @@ -5,6 +5,7 @@ */ import { Observable, Subscription } from 'rxjs'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ILicense } from '../../licensing/common/types'; export type LicenseState = { valid: false; message: string } | { valid: true }; diff --git a/x-pack/plugins/grokdebugger/README.md b/x-pack/plugins/grokdebugger/README.md new file mode 100644 index 0000000000000..80729b35500b6 --- /dev/null +++ b/x-pack/plugins/grokdebugger/README.md @@ -0,0 +1,6 @@ +# Grok Debugger + +## About + +This plugin helps users define [Grok patterns](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html), +which are particularly useful for ingesting logs. \ No newline at end of file diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index bd845b0a7d9a7..3d430cf31621e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -14,17 +14,77 @@ export const DEFAULT_POLICY: PolicyFromES = { version: 1, modified_date: Date.now().toString(), policy: { - name: '', + name: 'my_policy', + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + }, + }, + name: 'my_policy', +}; + +export const POLICY_WITH_MIGRATE_OFF: PolicyFromES = { + version: 1, + modified_date: Date.now().toString(), + policy: { + name: 'my_policy', + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + warm: { + actions: { + migrate: { enabled: false }, + }, + }, + }, + }, + name: 'my_policy', +}; + +export const POLICY_WITH_INCLUDE_EXCLUDE: PolicyFromES = { + version: 1, + modified_date: Date.now().toString(), + policy: { + name: 'my_policy', phases: { hot: { min_age: '123ms', actions: { - rollover: {}, + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + warm: { + actions: { + allocate: { + include: { + abc: '123', + }, + exclude: { + def: '456', + }, + }, }, }, }, }, - name: '', + name: 'my_policy', }; export const DELETE_PHASE_POLICY: PolicyFromES = { @@ -60,3 +120,58 @@ export const DELETE_PHASE_POLICY: PolicyFromES = { }, name: POLICY_NAME, }; + +export const POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION: PolicyFromES = { + version: 1, + modified_date: Date.now().toString(), + policy: { + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_size: '50gb', + }, + }, + }, + warm: { + actions: { + allocate: { + require: {}, + include: { test: '123' }, + exclude: {}, + }, + }, + }, + cold: { + actions: { + migrate: { enabled: false }, + }, + }, + }, + name: POLICY_NAME, + }, + name: POLICY_NAME, +}; + +export const POLICY_WITH_NODE_ROLE_ALLOCATION: PolicyFromES = { + version: 1, + modified_date: Date.now().toString(), + policy: { + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_size: '50gb', + }, + }, + }, + warm: { + actions: {}, + }, + }, + name: POLICY_NAME, + }, + name: POLICY_NAME, +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 0cfccba761309..ad61641ea1e36 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -9,11 +9,16 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBedConfig } from '../../../../../test_utils'; +import { EditPolicy } from '../../../public/application/sections/edit_policy'; +import { DataTierAllocationType } from '../../../public/application/sections/edit_policy/types'; + +import { Phases as PolicyPhases } from '../../../common/types'; + +type Phases = keyof PolicyPhases; + import { POLICY_NAME } from './constants'; import { TestSubjects } from '../helpers'; -import { EditPolicy } from '../../../public/application/sections/edit_policy'; - jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -52,7 +57,23 @@ export type EditPolicyTestBed = SetupReturn extends Promise ? U : Setup export const setup = async () => { const testBed = await initTestBed(); - const { find, component } = testBed; + const { find, component, form } = testBed; + + const createFormToggleAction = (dataTestSubject: string) => async (checked: boolean) => { + await act(async () => { + form.toggleEuiSwitch(dataTestSubject, checked); + }); + component.update(); + }; + + function createFormSetValueAction(dataTestSubject: string) { + return async (value: V) => { + await act(async () => { + form.setInputValue(dataTestSubject, value); + }); + component.update(); + }; + } const setWaitForSnapshotPolicy = async (snapshotPolicyName: string) => { act(() => { @@ -68,12 +89,7 @@ export const setup = async () => { component.update(); }; - const toggleRollover = async (checked: boolean) => { - await act(async () => { - find('rolloverSwitch').simulate('click', { target: { checked } }); - }); - component.update(); - }; + const toggleRollover = createFormToggleAction('rolloverSwitch'); const setMaxSize = async (value: string, units?: string) => { await act(async () => { @@ -87,12 +103,7 @@ export const setup = async () => { component.update(); }; - const setMaxDocs = async (value: string) => { - await act(async () => { - find('hot-selectedMaxDocuments').simulate('change', { target: { value } }); - }); - component.update(); - }; + const setMaxDocs = createFormSetValueAction('hot-selectedMaxDocuments'); const setMaxAge = async (value: string, units?: string) => { await act(async () => { @@ -104,34 +115,60 @@ export const setup = async () => { component.update(); }; - const toggleForceMerge = (phase: string) => async (checked: boolean) => { - await act(async () => { - find(`${phase}-forceMergeSwitch`).simulate('click', { target: { checked } }); + const toggleForceMerge = (phase: Phases) => createFormToggleAction(`${phase}-forceMergeSwitch`); + + const setForcemergeSegmentsCount = (phase: Phases) => + createFormSetValueAction(`${phase}-selectedForceMergeSegments`); + + const setBestCompression = (phase: Phases) => createFormToggleAction(`${phase}-bestCompression`); + + const setIndexPriority = (phase: Phases) => + createFormSetValueAction(`${phase}-phaseIndexPriority`); + + const enable = (phase: Phases) => createFormToggleAction(`enablePhaseSwitch-${phase}`); + + const warmPhaseOnRollover = createFormToggleAction(`warm-warmPhaseOnRollover`); + + const setMinAgeValue = (phase: Phases) => createFormSetValueAction(`${phase}-selectedMinimumAge`); + + const setMinAgeUnits = (phase: Phases) => + createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`); + + const setDataAllocation = (phase: Phases) => async (value: DataTierAllocationType) => { + act(() => { + find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click'); }); component.update(); - }; - - const setForcemergeSegmentsCount = (phase: string) => async (value: string) => { await act(async () => { - find(`${phase}-selectedForceMergeSegments`).simulate('change', { target: { value } }); + switch (value) { + case 'node_roles': + find(`${phase}-dataTierAllocationControls.defaultDataAllocationOption`).simulate('click'); + break; + case 'node_attrs': + find(`${phase}-dataTierAllocationControls.customDataAllocationOption`).simulate('click'); + break; + default: + find(`${phase}-dataTierAllocationControls.noneDataAllocationOption`).simulate('click'); + } }); component.update(); }; - const setBestCompression = (phase: string) => async (checked: boolean) => { - await act(async () => { - find(`${phase}-bestCompression`).simulate('click', { target: { checked } }); - }); - component.update(); + const setSelectedNodeAttribute = (phase: Phases) => + createFormSetValueAction(`${phase}-selectedNodeAttrs`); + + const setReplicas = (phase: Phases) => async (value: string) => { + await createFormToggleAction(`${phase}-setReplicasSwitch`)(true); + await createFormSetValueAction(`${phase}-selectedReplicaCount`)(value); }; - const setIndexPriority = (phase: string) => async (value: string) => { - await act(async () => { - find(`${phase}-phaseIndexPriority`).simulate('change', { target: { value } }); - }); - component.update(); + const setShrink = async (value: string) => { + await createFormToggleAction('shrinkSwitch')(true); + await createFormSetValueAction('warm-selectedPrimaryShardCount')(value); }; + const setFreeze = createFormToggleAction('freezeSwitch'); + return { ...testBed, actions: { @@ -147,6 +184,30 @@ export const setup = async () => { setBestCompression: setBestCompression('hot'), setIndexPriority: setIndexPriority('hot'), }, + warm: { + enable: enable('warm'), + warmPhaseOnRollover, + setMinAgeValue: setMinAgeValue('warm'), + setMinAgeUnits: setMinAgeUnits('warm'), + setDataAllocation: setDataAllocation('warm'), + setSelectedNodeAttribute: setSelectedNodeAttribute('warm'), + setReplicas: setReplicas('warm'), + setShrink, + toggleForceMerge: toggleForceMerge('warm'), + setForcemergeSegments: setForcemergeSegmentsCount('warm'), + setBestCompression: setBestCompression('warm'), + setIndexPriority: setIndexPriority('warm'), + }, + cold: { + enable: enable('cold'), + setMinAgeValue: setMinAgeValue('cold'), + setMinAgeUnits: setMinAgeUnits('cold'), + setDataAllocation: setDataAllocation('cold'), + setSelectedNodeAttribute: setSelectedNodeAttribute('cold'), + setReplicas: setReplicas('cold'), + setFreeze, + setIndexPriority: setIndexPriority('cold'), + }, }, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 3cbc2d982566e..11fadf51f27f8 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -15,6 +15,10 @@ import { NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME, DEFAULT_POLICY, + POLICY_WITH_MIGRATE_OFF, + POLICY_WITH_INCLUDE_EXCLUDE, + POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, + POLICY_WITH_NODE_ROLE_ALLOCATION, } from './constants'; window.scrollTo = jest.fn(); @@ -53,7 +57,8 @@ describe('', () => { await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toMatchInlineSnapshot(` + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy).toMatchInlineSnapshot(` Object { "name": "my_policy", "phases": Object { @@ -81,15 +86,254 @@ describe('', () => { test('disabling rollover', async () => { const { actions } = testBed; - await actions.hot.toggleRollover(false); + await actions.hot.toggleRollover(true); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toMatchInlineSnapshot(` + const policy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + const hotActions = policy.phases.hot.actions; + const rolloverAction = hotActions.rollover; + expect(rolloverAction).toBe(undefined); + expect(hotActions).toMatchInlineSnapshot(` + Object { + "set_priority": Object { + "priority": 100, + }, + } + `); + }); + }); + }); + + describe('warm phase', () => { + describe('serialization', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('default values', async () => { + const { actions } = testBed; + await actions.warm.enable(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm; + expect(warmPhase).toMatchInlineSnapshot(` + Object { + "actions": Object { + "set_priority": Object { + "priority": 50, + }, + }, + "min_age": "0ms", + } + `); + }); + + test('setting all values', async () => { + const { actions } = testBed; + await actions.warm.enable(true); + await actions.warm.setMinAgeValue('123'); + await actions.warm.setMinAgeUnits('d'); + await actions.warm.setDataAllocation('node_attrs'); + await actions.warm.setSelectedNodeAttribute('test:123'); + await actions.warm.setReplicas('123'); + await actions.warm.setShrink('123'); + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegments('123'); + await actions.warm.setBestCompression(true); + await actions.warm.setIndexPriority('123'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + // Check shape of entire policy + expect(entirePolicy).toMatchInlineSnapshot(` Object { "name": "my_policy", "phases": Object { "hot": Object { "actions": Object { + "rollover": Object { + "max_age": "30d", + "max_size": "50gb", + }, + "set_priority": Object { + "priority": 100, + }, + }, + "min_age": "0ms", + }, + "warm": Object { + "actions": Object { + "allocate": Object { + "number_of_replicas": 123, + "require": Object { + "test": "123", + }, + }, + "forcemerge": Object { + "index_codec": "best_compression", + "max_num_segments": 123, + }, + "set_priority": Object { + "priority": 123, + }, + "shrink": Object { + "number_of_shards": 123, + }, + }, + "min_age": "123d", + }, + }, + } + `); + }); + + test('setting warm phase on rollover to "true"', async () => { + const { actions } = testBed; + await actions.warm.enable(true); + await actions.warm.warmPhaseOnRollover(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhaseMinAge = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm + .min_age; + expect(warmPhaseMinAge).toBe(undefined); + }); + }); + + describe('policy with include and exclude', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_INCLUDE_EXCLUDE]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('preserves include, exclude allocation settings', async () => { + const { actions } = testBed; + await actions.warm.setDataAllocation('node_attrs'); + await actions.warm.setSelectedNodeAttribute('test:123'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhaseAllocate = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm + .actions.allocate; + expect(warmPhaseAllocate).toMatchInlineSnapshot(` + Object { + "exclude": Object { + "def": "456", + }, + "include": Object { + "abc": "123", + }, + "require": Object { + "test": "123", + }, + } + `); + }); + }); + }); + + describe('cold phase', () => { + describe('serialization', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('default values', async () => { + const { actions } = testBed; + + await actions.cold.enable(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.cold).toMatchInlineSnapshot(` + Object { + "actions": Object { + "set_priority": Object { + "priority": 0, + }, + }, + "min_age": "0d", + } + `); + }); + + test('setting all values', async () => { + const { actions } = testBed; + + await actions.cold.enable(true); + await actions.cold.setMinAgeValue('123'); + await actions.cold.setMinAgeUnits('s'); + await actions.cold.setDataAllocation('node_attrs'); + await actions.cold.setSelectedNodeAttribute('test:123'); + await actions.cold.setReplicas('123'); + await actions.cold.setFreeze(true); + await actions.cold.setIndexPriority('123'); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toMatchInlineSnapshot(` + Object { + "name": "my_policy", + "phases": Object { + "cold": Object { + "actions": Object { + "allocate": Object { + "number_of_replicas": 123, + "require": Object { + "test": "123", + }, + }, + "freeze": Object {}, + "set_priority": Object { + "priority": 123, + }, + }, + "min_age": "123s", + }, + "hot": Object { + "actions": Object { + "rollover": Object { + "max_age": "30d", + "max_size": "50gb", + }, "set_priority": Object { "priority": 100, }, @@ -216,4 +460,104 @@ describe('', () => { expect(testBed.find('policiesErrorCallout').exists()).toBeTruthy(); }); }); + + describe('data allocation', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_MIGRATE_OFF]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('setting node_attr based allocation, but not selecting node attribute', async () => { + const { actions } = testBed; + await actions.warm.setDataAllocation('node_attrs'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm; + + expect(warmPhase.actions.migrate).toEqual({ enabled: false }); + }); + + describe('node roles', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ROLE_ALLOCATION]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('detecting use of the recommended allocation type', () => { + const { find } = testBed; + const selectedDataAllocation = find( + 'warm-dataTierAllocationControls.dataTierSelect' + ).text(); + expect(selectedDataAllocation).toBe('Use warm nodes (recommended)'); + }); + + test('setting replicas serialization', async () => { + const { actions } = testBed; + await actions.warm.setReplicas('123'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhaseActions = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm + .actions; + expect(warmPhaseActions).toMatchInlineSnapshot(` + Object { + "allocate": Object { + "number_of_replicas": 123, + }, + "set_priority": Object { + "priority": 50, + }, + } + `); + }); + }); + describe('node attr and none', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('detecting use of the custom allocation type', () => { + const { find } = testBed; + expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom'); + }); + test('detecting use of the "off" allocation type', () => { + const { find } = testBed; + expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); + }); + }); + }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts index 04f58f93939ca..c7a493ce80d96 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts @@ -6,6 +6,7 @@ import { fakeServer, SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; +import { ListNodesRouteResponse } from '../../../common/types'; export const init = () => { const server = fakeServer.create(); @@ -38,8 +39,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setListNodes = (body: ListNodesRouteResponse) => { + server.respondWith('GET', `${API_BASE_PATH}/nodes/list`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadPolicies, setLoadSnapshotPolicies, + setListNodes, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index c6d27ca890b54..e8ebc2963d16a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -17,4 +17,5 @@ export type TestSubjects = | 'hot-selectedMaxDocuments' | 'hot-selectedMaxAge' | 'hot-selectedMaxAgeUnits' + | 'freezeSwitch' | string; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index a88c6ebc47683..4a3fedfb264ac 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -30,9 +30,7 @@ import { init as initUiMetric } from '../../public/application/services/ui_metri import { init as initNotification } from '../../public/application/services/notification'; import { PolicyFromES } from '../../common/types'; import { - positiveNumbersAboveZeroErrorMessage, positiveNumberRequiredMessage, - numberRequiredMessage, policyNameRequiredMessage, policyNameStartsWithUnderscoreErrorMessage, policyNameContainsCommaErrorMessage, @@ -40,6 +38,8 @@ import { policyNameMustBeDifferentErrorMessage, policyNameAlreadyUsedErrorMessage, } from '../../public/application/services/policies/policy_validation'; + +import { i18nTexts } from '../../public/application/sections/edit_policy/i18n_texts'; import { editPolicyHelpers } from './helpers'; // @ts-ignore @@ -89,13 +89,13 @@ const activatePhase = async (rendered: ReactWrapper, phase: string) => { }); rendered.update(); }; -const openNodeAttributesSection = (rendered: ReactWrapper, phase: string) => { +const openNodeAttributesSection = async (rendered: ReactWrapper, phase: string) => { const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`); - act(() => { + await act(async () => { findTestSubject(getControls(), 'dataTierSelect').simulate('click'); }); rendered.update(); - act(() => { + await act(async () => { findTestSubject(getControls(), 'customDataAllocationOption').simulate('click'); }); rendered.update(); @@ -120,25 +120,23 @@ const noRollover = async (rendered: ReactWrapper) => { rendered.update(); }; const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => { - return rendered.find(`select#${phase}-selectedNodeAttrs`); + return findTestSubject(rendered, `${phase}-selectedNodeAttrs`); }; const setPolicyName = (rendered: ReactWrapper, policyName: string) => { const policyNameField = findTestSubject(rendered, 'policyNameField'); policyNameField.simulate('change', { target: { value: policyName } }); rendered.update(); }; -const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string | number) => { +const setPhaseAfterLegacy = (rendered: ReactWrapper, phase: string, after: string | number) => { const afterInput = rendered.find(`input#${phase}-selectedMinimumAge`); afterInput.simulate('change', { target: { value: after } }); rendered.update(); }; -const setPhaseIndexPriorityLegacy = ( - rendered: ReactWrapper, - phase: string, - priority: string | number -) => { - const priorityInput = rendered.find(`input#${phase}-phaseIndexPriority`); - priorityInput.simulate('change', { target: { value: priority } }); +const setPhaseAfter = async (rendered: ReactWrapper, phase: string, after: string | number) => { + const afterInput = findTestSubject(rendered, `${phase}-selectedMinimumAge`); + await act(async () => { + afterInput.simulate('change', { target: { value: after } }); + }); rendered.update(); }; const setPhaseIndexPriority = async ( @@ -172,10 +170,11 @@ describe('edit policy', () => { * any validation errors. This helper advances timers and can trigger component * state changes. */ - const waitForFormLibValidation = () => { + const waitForFormLibValidation = (rendered: ReactWrapper) => { act(() => { - jest.advanceTimersByTime(1000); + jest.runAllTimers(); }); + rendered.update(); }; beforeEach(() => { @@ -288,13 +287,12 @@ describe('edit policy', () => { await act(async () => { maxSizeInput.simulate('change', { target: { value: '' } }); }); - waitForFormLibValidation(); + waitForFormLibValidation(rendered); const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); await act(async () => { maxAgeInput.simulate('change', { target: { value: '' } }); }); - waitForFormLibValidation(); - rendered.update(); + waitForFormLibValidation(rendered); await save(rendered); expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeTruthy(); }); @@ -305,9 +303,9 @@ describe('edit policy', () => { await act(async () => { maxSizeInput.simulate('change', { target: { value: '-1' } }); }); - waitForFormLibValidation(); + waitForFormLibValidation(rendered); rendered.update(); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show number above 0 required error when trying to save with 0 for max size', async () => { const rendered = mountWithIntl(component); @@ -316,9 +314,8 @@ describe('edit policy', () => { await act(async () => { maxSizeInput.simulate('change', { target: { value: '-1' } }); }); - waitForFormLibValidation(); - rendered.update(); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show number above 0 required error when trying to save with -1 for max age', async () => { const rendered = mountWithIntl(component); @@ -327,9 +324,8 @@ describe('edit policy', () => { await act(async () => { maxAgeInput.simulate('change', { target: { value: '-1' } }); }); - waitForFormLibValidation(); - rendered.update(); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show number above 0 required error when trying to save with 0 for max age', async () => { const rendered = mountWithIntl(component); @@ -338,9 +334,8 @@ describe('edit policy', () => { await act(async () => { maxAgeInput.simulate('change', { target: { value: '0' } }); }); - waitForFormLibValidation(); - rendered.update(); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show forcemerge input when rollover enabled', () => { const rendered = mountWithIntl(component); @@ -351,8 +346,7 @@ describe('edit policy', () => { const rendered = mountWithIntl(component); setPolicyName(rendered, 'mypolicy'); await noRollover(rendered); - waitForFormLibValidation(); - rendered.update(); + waitForFormLibValidation(rendered); expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy(); }); test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => { @@ -366,9 +360,8 @@ describe('edit policy', () => { await act(async () => { forcemergeInput.simulate('change', { target: { value: '0' } }); }); - waitForFormLibValidation(); - rendered.update(); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => { const rendered = mountWithIntl(component); @@ -379,19 +372,17 @@ describe('edit policy', () => { await act(async () => { forcemergeInput.simulate('change', { target: { value: '-1' } }); }); - waitForFormLibValidation(); - rendered.update(); + waitForFormLibValidation(rendered); await save(rendered); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show positive number required error when trying to save with -1 for index priority', async () => { const rendered = mountWithIntl(component); await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await setPhaseIndexPriority(rendered, 'hot', '-1'); - waitForFormLibValidation(); - rendered.update(); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); }); describe('warm phase', () => { @@ -408,17 +399,17 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', ''); - await save(rendered); - expectedErrorMessages(rendered, [numberRequiredMessage]); + await setPhaseAfter(rendered, 'warm', ''); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); test('should allow 0 for phase timing', async () => { const rendered = mountWithIntl(component); await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', '0'); - await save(rendered); + await setPhaseAfter(rendered, 'warm', '0'); + waitForFormLibValidation(rendered); expectedErrorMessages(rendered, []); }); test('should show positive number required error when trying to save warm phase with -1 for after', async () => { @@ -426,75 +417,87 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', '-1'); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + await setPhaseAfter(rendered, 'warm', '-1'); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => { const rendered = mountWithIntl(component); await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', '1'); - setPhaseIndexPriorityLegacy(rendered, 'warm', '-1'); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + await setPhaseAfter(rendered, 'warm', '1'); + await setPhaseAfter(rendered, 'warm', '-1'); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => { const rendered = mountWithIntl(component); await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - findTestSubject(rendered, 'shrinkSwitch').simulate('click'); - rendered.update(); - setPhaseAfter(rendered, 'warm', '1'); - const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount'); - shrinkInput.simulate('change', { target: { value: '0' } }); + act(() => { + findTestSubject(rendered, 'shrinkSwitch').simulate('click'); + }); rendered.update(); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + await setPhaseAfter(rendered, 'warm', '1'); + const shrinkInput = findTestSubject(rendered, 'warm-selectedPrimaryShardCount'); + await act(async () => { + shrinkInput.simulate('change', { target: { value: '0' } }); + }); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => { const rendered = mountWithIntl(component); await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', '1'); - findTestSubject(rendered, 'shrinkSwitch').simulate('click'); - rendered.update(); - const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount'); - shrinkInput.simulate('change', { target: { value: '-1' } }); + await setPhaseAfter(rendered, 'warm', '1'); + act(() => { + findTestSubject(rendered, 'shrinkSwitch').simulate('click'); + }); rendered.update(); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + const shrinkInput = findTestSubject(rendered, 'warm-selectedPrimaryShardCount'); + await act(async () => { + shrinkInput.simulate('change', { target: { value: '-1' } }); + }); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => { const rendered = mountWithIntl(component); await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', '1'); - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); + await setPhaseAfter(rendered, 'warm', '1'); + act(() => { + findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); + }); rendered.update(); const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - forcemergeInput.simulate('change', { target: { value: '0' } }); - rendered.update(); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + await act(async () => { + forcemergeInput.simulate('change', { target: { value: '0' } }); + }); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => { const rendered = mountWithIntl(component); await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', '1'); - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); + await setPhaseAfter(rendered, 'warm', '1'); + await act(async () => { + findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); + }); rendered.update(); const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - forcemergeInput.simulate('change', { target: { value: '-1' } }); - rendered.update(); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumbersAboveZeroErrorMessage]); + await act(async () => { + forcemergeInput.simulate('change', { target: { value: '-1' } }); + }); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); test('should show spinner for node attributes input when loading', async () => { server.respondImmediately = false; @@ -517,7 +520,7 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - openNodeAttributesSection(rendered, 'warm'); + await openNodeAttributesSection(rendered, 'warm'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); }); @@ -527,7 +530,7 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - openNodeAttributesSection(rendered, 'warm'); + await openNodeAttributesSection(rendered, 'warm'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); expect(nodeAttributesSelect.exists()).toBeTruthy(); @@ -539,13 +542,15 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - openNodeAttributesSection(rendered, 'warm'); + await openNodeAttributesSection(rendered, 'warm'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); expect(nodeAttributesSelect.exists()).toBeTruthy(); expect(findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); expect(nodeAttributesSelect.find('option').length).toBe(2); - nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); + await act(async () => { + nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); + }); rendered.update(); const flyoutButton = findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton'); expect(flyoutButton.exists()).toBeTruthy(); @@ -608,8 +613,9 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); - setPhaseAfter(rendered, 'cold', '0'); - await save(rendered); + await setPhaseAfter(rendered, 'cold', '0'); + waitForFormLibValidation(rendered); + rendered.update(); expectedErrorMessages(rendered, []); }); test('should show positive number required error when trying to save cold phase with -1 for after', async () => { @@ -617,9 +623,9 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); - setPhaseAfter(rendered, 'cold', '-1'); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + await setPhaseAfter(rendered, 'cold', '-1'); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); test('should show spinner for node attributes input when loading', async () => { server.respondImmediately = false; @@ -642,7 +648,7 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - openNodeAttributesSection(rendered, 'cold'); + await openNodeAttributesSection(rendered, 'cold'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); }); @@ -652,7 +658,7 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - openNodeAttributesSection(rendered, 'cold'); + await openNodeAttributesSection(rendered, 'cold'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); expect(nodeAttributesSelect.exists()).toBeTruthy(); @@ -664,7 +670,7 @@ describe('edit policy', () => { setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - openNodeAttributesSection(rendered, 'cold'); + await openNodeAttributesSection(rendered, 'cold'); expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); expect(nodeAttributesSelect.exists()).toBeTruthy(); @@ -685,10 +691,10 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); - setPhaseAfter(rendered, 'cold', '1'); - setPhaseIndexPriorityLegacy(rendered, 'cold', '-1'); - await save(rendered); - expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); + await setPhaseAfter(rendered, 'cold', '1'); + await setPhaseIndexPriority(rendered, 'cold', '-1'); + waitForFormLibValidation(rendered); + expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); test('should show default allocation warning when no node roles are found', async () => { http.setupNodeListResponse({ @@ -736,7 +742,7 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'delete'); - setPhaseAfter(rendered, 'delete', '0'); + setPhaseAfterLegacy(rendered, 'delete', '0'); await save(rendered); expectedErrorMessages(rendered, []); }); @@ -745,7 +751,7 @@ describe('edit policy', () => { await noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'delete'); - setPhaseAfter(rendered, 'delete', '-1'); + setPhaseAfterLegacy(rendered, 'delete', '-1'); await save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 152c5e4e9e0d7..5692decbbf7a8 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -35,6 +35,19 @@ export interface SerializedPhase { }; } +export interface MigrateAction { + /** + * If enabled is ever set it will probably only be set to `false` because the default value + * for this is `true`. Rather leave unspecified for true when serialising. + */ + enabled: boolean; +} + +export interface SerializedActionWithAllocation { + allocate?: AllocateAction; + migrate?: MigrateAction; +} + export interface SerializedHotPhase extends SerializedPhase { actions: { rollover?: { @@ -59,7 +72,7 @@ export interface SerializedWarmPhase extends SerializedPhase { set_priority?: { priority: number | null; }; - migrate?: { enabled: boolean }; + migrate?: MigrateAction; }; } @@ -70,7 +83,7 @@ export interface SerializedColdPhase extends SerializedPhase { set_priority?: { priority: number | null; }; - migrate?: { enabled: boolean }; + migrate?: MigrateAction; }; } @@ -87,18 +100,11 @@ export interface SerializedDeletePhase extends SerializedPhase { export interface AllocateAction { number_of_replicas?: number; - include: {}; - exclude: {}; + include?: {}; + exclude?: {}; require?: { [attribute: string]: string; }; - migrate?: { - /** - * If enabled is ever set it will only be set to `false` because the default value - * for this is `true`. Rather leave unspecified for true. - */ - enabled: false; - }; } export interface ForcemergeAction { @@ -110,8 +116,6 @@ export interface ForcemergeAction { export interface LegacyPolicy { name: string; phases: { - warm: WarmPhase; - cold: ColdPhase; delete: DeletePhase; }; } @@ -154,25 +158,6 @@ export interface PhaseWithForcemergeAction { bestCompressionEnabled: boolean; } -export interface WarmPhase - extends CommonPhaseSettings, - PhaseWithMinAge, - PhaseWithAllocationAction, - PhaseWithIndexPriority, - PhaseWithForcemergeAction { - warmPhaseOnRollover: boolean; - shrinkEnabled: boolean; - selectedPrimaryShardCount: string; -} - -export interface ColdPhase - extends CommonPhaseSettings, - PhaseWithMinAge, - PhaseWithAllocationAction, - PhaseWithIndexPriority { - freezeEnabled: boolean; -} - export interface DeletePhase extends CommonPhaseSettings, PhaseWithMinAge { waitForSnapshotPolicy: string; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index c919331082ec3..23d7387aa7076 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -4,16 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - SerializedPhase, - ColdPhase, - DeletePhase, - WarmPhase, - SerializedPolicy, -} from '../../../common/types'; +import { SerializedPhase, DeletePhase, SerializedPolicy } from '../../../common/types'; export const defaultSetPriority: string = '100'; +export const defaultPhaseIndexPriority: string = '50'; + export const defaultPolicy: SerializedPolicy = { name: '', phases: { @@ -28,33 +24,6 @@ export const defaultPolicy: SerializedPolicy = { }, }; -export const defaultNewWarmPhase: WarmPhase = { - phaseEnabled: false, - forceMergeEnabled: false, - selectedForceMergeSegments: '', - bestCompressionEnabled: false, - selectedMinimumAge: '0', - selectedMinimumAgeUnits: 'd', - selectedNodeAttrs: '', - shrinkEnabled: false, - selectedPrimaryShardCount: '', - selectedReplicaCount: '', - warmPhaseOnRollover: true, - phaseIndexPriority: '50', - dataTierAllocationType: 'default', -}; - -export const defaultNewColdPhase: ColdPhase = { - phaseEnabled: false, - selectedMinimumAge: '0', - selectedMinimumAgeUnits: 'd', - selectedNodeAttrs: '', - selectedReplicaCount: '', - freezeEnabled: false, - phaseIndexPriority: '0', - dataTierAllocationType: 'default', -}; - export const defaultNewDeletePhase: DeletePhase = { phaseEnabled: false, selectedMinimumAge: '0', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts index 4067ad97fc43b..20ac439e9964f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts @@ -4,31 +4,63 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataTierAllocationType, AllocateAction } from '../../../../common/types'; +import { DataTierAllocationType, AllocateAction, MigrateAction } from '../../../../common/types'; /** * Determine what deserialized state the policy config represents. * * See {@DataTierAllocationType} for more information. */ -export const determineDataTierAllocationType = ( - allocateAction?: AllocateAction +export const determineDataTierAllocationTypeLegacy = ( + actions: { + allocate?: AllocateAction; + migrate?: MigrateAction; + } = {} ): DataTierAllocationType => { - if (!allocateAction) { - return 'default'; - } + const { allocate, migrate } = actions; - if (allocateAction.migrate?.enabled === false) { + if (migrate?.enabled === false) { return 'none'; } + if (!allocate) { + return 'default'; + } + if ( - (allocateAction.require && Object.keys(allocateAction.require).length) || - (allocateAction.include && Object.keys(allocateAction.include).length) || - (allocateAction.exclude && Object.keys(allocateAction.exclude).length) + (allocate.require && Object.keys(allocate.require).length) || + (allocate.include && Object.keys(allocate.include).length) || + (allocate.exclude && Object.keys(allocate.exclude).length) ) { return 'custom'; } return 'default'; }; + +export const determineDataTierAllocationType = ( + actions: { + allocate?: AllocateAction; + migrate?: MigrateAction; + } = {} +) => { + const { allocate, migrate } = actions; + + if (migrate?.enabled === false) { + return 'none'; + } + + if (!allocate) { + return 'node_roles'; + } + + if ( + (allocate.require && Object.keys(allocate.require).length) || + (allocate.include && Object.keys(allocate.include).length) || + (allocate.exclude && Object.keys(allocate.exclude).length) + ) { + return 'node_attrs'; + } + + return 'node_roles'; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/index.ts index 87f2cbc08ecc0..ac1aae270628d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/index.ts @@ -7,3 +7,5 @@ export * from './determine_allocation_type'; export * from './get_available_node_roles_for_phase'; + +export * from './is_node_role_first_preference'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx deleted file mode 100644 index fc87b553ba521..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FunctionComponent } from 'react'; -import { EuiCallOut, EuiLink } from '@elastic/eui'; - -import { useKibana } from '../../../../../shared_imports'; - -const deployment = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body.elasticDeploymentLink', - { - defaultMessage: 'deployment', - } -); - -const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.coldTierTitle', { - defaultMessage: 'Create a cold tier', - }), - body: (deploymentUrl?: string) => { - return ( - - {deployment} - - ) : ( - deployment - ), - }} - /> - ); - }, -}; - -export const CloudDataTierCallout: FunctionComponent = () => { - const { - services: { cloud }, - } = useKibana(); - - return ( - - {i18nTexts.body(cloud?.cloudDeploymentUrl)} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx deleted file mode 100644 index f58f36fc45a0c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FunctionComponent, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiText, EuiFormRow, EuiSpacer, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; - -import { DataTierAllocationType } from '../../../../../../common/types'; -import { NodeAllocation } from './node_allocation'; -import { SharedProps } from './types'; - -import './data_tier_allocation.scss'; - -type SelectOptions = EuiSuperSelectOption; - -const i18nTexts = { - allocationFieldLabel: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.allocationFieldLabel', - { defaultMessage: 'Data tier options' } - ), - allocationOptions: { - warm: { - default: { - input: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input', - { defaultMessage: 'Use warm nodes (recommended)' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText', - { defaultMessage: 'Move data to nodes in the warm tier.' } - ), - }, - none: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.input', - { defaultMessage: 'Off' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText', - { defaultMessage: 'Do not move data in the warm phase.' } - ), - }, - custom: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input', - { defaultMessage: 'Custom' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText', - { defaultMessage: 'Move data based on node attributes.' } - ), - }, - }, - cold: { - default: { - input: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input', - { defaultMessage: 'Use cold nodes (recommended)' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText', - { defaultMessage: 'Move data to nodes in the cold tier.' } - ), - }, - none: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input', - { defaultMessage: 'Off' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText', - { defaultMessage: 'Do not move data in the cold phase.' } - ), - }, - custom: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input', - { defaultMessage: 'Custom' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText', - { defaultMessage: 'Move data based on node attributes.' } - ), - }, - }, - }, -}; - -export const DataTierAllocation: FunctionComponent = (props) => { - const { phaseData, setPhaseData, phase, hasNodeAttributes, disableDataTierOption } = props; - - useEffect(() => { - if (disableDataTierOption && phaseData.dataTierAllocationType === 'default') { - /** - * @TODO - * This is a slight hack because we only know we should disable the "default" option further - * down the component tree (i.e., after the policy has been deserialized). - * - * We reset the value to "custom" if we deserialized to "default". - * - * It would be better if we had all the information we needed before deserializing and - * were able to handle this at the deserialization step instead of patching further down - * the component tree - this should be a future refactor. - */ - setPhaseData('dataTierAllocationType', 'custom'); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( -
- - setPhaseData('dataTierAllocationType', value)} - options={ - [ - disableDataTierOption - ? undefined - : { - 'data-test-subj': 'defaultDataAllocationOption', - value: 'default', - inputDisplay: i18nTexts.allocationOptions[phase].default.input, - dropdownDisplay: ( - <> - {i18nTexts.allocationOptions[phase].default.input} - -

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

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

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

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

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

-
- - ), - }, - ].filter(Boolean) as SelectOptions[] - } - /> -
- {phaseData.dataTierAllocationType === 'custom' && hasNodeAttributes && ( - <> - -
- -
- - )} -
- ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx deleted file mode 100644 index 0b0dbe273c024..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/forcemerge.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * PLEASE NOTE: This component is currently duplicated. A version of this component wired up with - * the form lib lives in ./phases/shared - */ - -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiDescribedFormGroup, - EuiFieldNumber, - EuiFormRow, - EuiSpacer, - EuiSwitch, - EuiTextColor, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { LearnMoreLink } from './learn_more_link'; -import { ErrableFormRow } from './form_errors'; -import { Phases, PhaseWithForcemergeAction } from '../../../../../common/types'; -import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; - -const forcemergeLabel = i18n.translate('xpack.indexLifecycleMgmt.forcemerge.enableLabel', { - defaultMessage: 'Force merge data', -}); - -const bestCompressionLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel', - { - defaultMessage: 'Compress stored fields', - } -); - -interface Props { - errors?: PhaseValidationErrors; - phase: keyof Phases & string; - phaseData: PhaseWithForcemergeAction; - setPhaseData: (dataKey: keyof PhaseWithForcemergeAction, value: boolean | string) => void; - isShowingErrors: boolean; -} - -export const Forcemerge: React.FunctionComponent = ({ - errors, - phaseData, - phase, - setPhaseData, - isShowingErrors, -}) => { - return ( - - - - } - description={ - - {' '} - - - } - titleSize="xs" - fullWidth - > - { - setPhaseData('forceMergeEnabled', e.target.checked); - }} - aria-controls="forcemergeContent" - /> - - -
- {phaseData.forceMergeEnabled ? ( - <> - - { - setPhaseData('selectedForceMergeSegments', e.target.value); - }} - min={1} - /> - - - } - > - { - setPhaseData('bestCompressionEnabled', e.target.checked); - }} - /> - - - ) : null} -
-
- ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index 04d9a6ef3cf67..a04608338718e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -7,20 +7,11 @@ export { ActiveBadge } from './active_badge'; export { ErrableFormRow } from './form_errors'; export { LearnMoreLink } from './learn_more_link'; -export { MinAgeInput } from './min_age_input'; +export { MinAgeInput } from './min_age_input_legacy'; export { OptionalLabel } from './optional_label'; export { PhaseErrorMessage } from './phase_error_message'; export { PolicyJsonFlyout } from './policy_json_flyout'; -export { SetPriorityInput } from './set_priority_input'; export { SnapshotPolicies } from './snapshot_policies'; -export { - DataTierAllocation, - NodeAllocation, - NodeAttrsDetails, - NodesDataProvider, - DefaultAllocationNotice, -} from './data_tier_allocation'; export { DescribedFormField } from './described_form_field'; -export { Forcemerge } from './forcemerge'; export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx deleted file mode 100644 index 7a22fb5bc1f0c..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase.tsx +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FunctionComponent, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; - -import { EuiFieldNumber, EuiDescribedFormGroup, EuiSwitch, EuiTextColor } from '@elastic/eui'; - -import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../../common/types'; - -import { useFormData } from '../../../../../shared_imports'; - -import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; - -import { - LearnMoreLink, - ActiveBadge, - PhaseErrorMessage, - OptionalLabel, - ErrableFormRow, - SetPriorityInput, - MinAgeInput, - DescribedFormField, -} from '../'; - -import { DataTierAllocationField, useRolloverPath } from './shared'; - -const i18nTexts = { - freezeLabel: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { - defaultMessage: 'Freeze index', - }), - dataTierAllocation: { - description: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.description', { - defaultMessage: - 'Move data to nodes optimized for less frequent, read-only access. Store data in the cold phase on less-expensive hardware.', - }), - }, -}; - -const coldProperty: keyof Phases = 'cold'; -const phaseProperty = (propertyName: keyof ColdPhaseInterface) => propertyName; - -interface Props { - setPhaseData: (key: keyof ColdPhaseInterface & string, value: string | boolean) => void; - phaseData: ColdPhaseInterface; - isShowingErrors: boolean; - errors?: PhaseValidationErrors; -} -export const ColdPhase: FunctionComponent = ({ - setPhaseData, - phaseData, - errors, - isShowingErrors, -}) => { - const [formData] = useFormData({ - watch: [useRolloverPath], - }); - - const hotPhaseRolloverEnabled = get(formData, useRolloverPath); - - return ( -
- <> - {/* Section title group; containing min age */} - -

- -

{' '} - {phaseData.phaseEnabled && !isShowingErrors ? : null} - -
- } - titleSize="s" - description={ - -

- -

- - } - id={`${coldProperty}-${phaseProperty('phaseEnabled')}`} - checked={phaseData.phaseEnabled} - onChange={(e) => { - setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); - }} - aria-controls="coldPhaseContent" - /> -
- } - fullWidth - > - {phaseData.phaseEnabled ? ( - - errors={errors} - phaseData={phaseData} - phase={coldProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - rolloverEnabled={hotPhaseRolloverEnabled} - /> - ) : null} - - {phaseData.phaseEnabled ? ( - - {/* Data tier allocation section */} - - - {/* Replicas section */} - - {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { - defaultMessage: 'Replicas', - })} - - } - description={i18n.translate( - 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', - { - defaultMessage: - 'Set the number of replicas. Remains the same as the previous phase by default.', - } - )} - switchProps={{ - label: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel', - { defaultMessage: 'Set replicas' } - ), - initialValue: Boolean(phaseData.selectedReplicaCount), - onChange: (v) => { - if (!v) { - setPhaseData('selectedReplicaCount', ''); - } - }, - }} - fullWidth - > - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.selectedReplicaCount} - > - { - setPhaseData(phaseProperty('selectedReplicaCount'), e.target.value); - }} - min={0} - /> - - - {/* Freeze section */} - - - - } - description={ - - {' '} - - - } - fullWidth - titleSize="xs" - > - { - setPhaseData(phaseProperty('freezeEnabled'), e.target.checked); - }} - label={i18nTexts.freezeLabel} - aria-label={i18nTexts.freezeLabel} - /> - - - errors={errors} - phaseData={phaseData} - phase={coldProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - /> - - ) : null} - -
- ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx new file mode 100644 index 0000000000000..84e955a91ad7c --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; + +import { EuiDescribedFormGroup, EuiTextColor } from '@elastic/eui'; + +import { Phases } from '../../../../../../../common/types'; + +import { + useFormData, + useFormContext, + UseField, + ToggleField, + NumericField, +} from '../../../../../../shared_imports'; + +import { useEditPolicyContext } from '../../../edit_policy_context'; + +import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, DescribedFormField } from '../../'; + +import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared'; + +const i18nTexts = { + dataTierAllocation: { + description: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.dataTier.description', { + defaultMessage: + 'Move data to nodes optimized for less frequent, read-only access. Store data in the cold phase on less-expensive hardware.', + }), + }, +}; + +const coldProperty: keyof Phases = 'cold'; + +const formFieldPaths = { + enabled: '_meta.cold.enabled', +}; + +export const ColdPhase: FunctionComponent = () => { + const { originalPolicy } = useEditPolicyContext(); + const form = useFormContext(); + + const [formData] = useFormData({ + watch: [formFieldPaths.enabled], + }); + + const enabled = get(formData, formFieldPaths.enabled); + const isShowingErrors = form.isValid === false; + + return ( +
+ <> + {/* Section title group; containing min age */} + +

+ +

{' '} + {enabled && !isShowingErrors ? : null} + +
+ } + titleSize="s" + description={ + <> +

+ +

+ + + } + fullWidth + > + {enabled && } + + {enabled && ( + <> + {/* Data tier allocation section */} + + + {/* Replicas section */} + + {i18n.translate('xpack.indexLifecycleMgmt.coldPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + 'data-test-subj': 'cold-setReplicasSwitch', + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean( + originalPolicy.phases.cold?.actions?.allocate?.number_of_replicas + ), + }} + fullWidth + > + + + {/* Freeze section */} + + + + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + > + + + + + )} + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/index.ts similarity index 79% rename from x-pack/plugins/ingest_pipelines/public/application/sections/shared/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/index.ts index 9326d13851387..df79607f33dbc 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/shared/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { attemptToURIDecode } from './attempt_to_uri_decode'; +export { ColdPhase } from './cold_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 6a5f946b89b58..a184ddf5148b9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FunctionComponent, useEffect, useState } from 'react'; +import React, { Fragment, FunctionComponent, useState } from 'react'; import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -27,25 +27,21 @@ import { NumericField, } from '../../../../../../shared_imports'; +import { i18nTexts } from '../../../i18n_texts'; + import { ROLLOVER_EMPTY_VALIDATION } from '../../../form_validations'; import { ROLLOVER_FORM_PATHS } from '../../../constants'; import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../'; -import { Forcemerge, SetPriorityInput } from '../shared'; +import { Forcemerge, SetPriorityInput, useRolloverPath } from '../shared'; import { maxSizeStoredUnits, maxAgeUnits } from './constants'; -import { i18nTexts } from './i18n_texts'; - -import { useRolloverPath } from '../shared'; - const hotProperty: keyof Phases = 'hot'; -export const HotPhase: FunctionComponent<{ setWarmPhaseOnRollover: (v: boolean) => void }> = ({ - setWarmPhaseOnRollover, -}) => { +export const HotPhase: FunctionComponent = () => { const form = useFormContext(); const [formData] = useFormData({ watch: useRolloverPath, @@ -55,10 +51,6 @@ export const HotPhase: FunctionComponent<{ setWarmPhaseOnRollover: (v: boolean) const isShowingErrors = form.isValid === false; const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); - useEffect(() => { - setWarmPhaseOnRollover(isRolloverEnabled ?? false); - }, [setWarmPhaseOnRollover, isRolloverEnabled]); - return ( <> -
{i18nTexts.rollOverConfigurationCallout.body}
+
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts deleted file mode 100644 index 6423b12b86dd2..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/i18n_texts.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const i18nTexts = { - maximumAgeRequiredMessage: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError', - { - defaultMessage: 'A maximum age is required.', - } - ), - maximumSizeRequiredMessage: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError', - { - defaultMessage: 'A maximum index size is required.', - } - ), - maximumDocumentsRequiredMessage: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError', - { - defaultMessage: 'Maximum documents is required.', - } - ), - rollOverConfigurationCallout: { - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.rolloverConfigurationError.title', { - defaultMessage: 'Invalid rollover configuration', - }), - body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.rolloverConfigurationError.body', { - defaultMessage: - 'A value for one of maximum size, maximum documents, or maximum age is required.', - }), - }, -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/cloud_data_tier_callout.tsx new file mode 100644 index 0000000000000..2dff55ac10de1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/cloud_data_tier_callout.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', { + defaultMessage: 'Create a cold tier', + }), + body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body', { + defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + }), +}; + +export const CloudDataTierCallout: FunctionComponent = () => { + return ( + + {i18nTexts.body} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.scss similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.scss rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.scss diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.tsx new file mode 100644 index 0000000000000..56af4bbac7a26 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.tsx @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { EuiText, EuiSpacer, EuiSuperSelectOption } from '@elastic/eui'; + +import { UseField, SuperSelectField, useFormData } from '../../../../../../../../shared_imports'; +import { PhaseWithAllocation } from '../../../../../../../../../common/types'; + +import { DataTierAllocationType } from '../../../../../types'; + +import { NodeAllocation } from './node_allocation'; +import { SharedProps } from './types'; + +import './data_tier_allocation.scss'; + +type SelectOptions = EuiSuperSelectOption; + +const i18nTexts = { + allocationOptions: { + warm: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input', + { defaultMessage: 'Use warm nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the warm tier.' } + ), + }, + none: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.input', + { defaultMessage: 'Off' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText', + { defaultMessage: 'Do not move data in the warm phase.' } + ), + }, + custom: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), + }, + }, + cold: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input', + { defaultMessage: 'Use cold nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the cold tier.' } + ), + }, + none: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input', + { defaultMessage: 'Off' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText', + { defaultMessage: 'Do not move data in the cold phase.' } + ), + }, + custom: { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), + }, + }, + }, +}; + +const getSelectOptions = (phase: PhaseWithAllocation, disableDataTierOption: boolean) => + [ + disableDataTierOption + ? undefined + : { + 'data-test-subj': 'defaultDataAllocationOption', + value: 'node_roles', + inputDisplay: i18nTexts.allocationOptions[phase].default.input, + dropdownDisplay: ( + <> + {i18nTexts.allocationOptions[phase].default.input} + +

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

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

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

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

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

+
+ + ), + }, + ].filter(Boolean) as SelectOptions[]; + +export const DataTierAllocation: FunctionComponent = (props) => { + const { phase, hasNodeAttributes, disableDataTierOption } = props; + + const dataTierAllocationTypePath = `_meta.${phase}.dataTierAllocationType`; + + const [formData] = useFormData({ + watch: dataTierAllocationTypePath, + }); + + const dataTierAllocationType = get(formData, dataTierAllocationTypePath); + + return ( +
+ + {(field) => { + /** + * We reset the value to "custom" if we deserialized to "default". + * + * It would be better if we had all the information we needed before deserializing and + * were able to handle this at the deserialization step instead of patching further down + * the component tree - this should be a future refactor. + */ + if (disableDataTierOption && field.value === 'node_roles') { + field.setValue('node_attrs'); + } + return ( + + ); + }} + + {dataTierAllocationType === 'node_attrs' && hasNodeAttributes && ( + <> + +
+ +
+ + )} +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/default_allocation_notice.tsx similarity index 97% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/default_allocation_notice.tsx index 3d0052c69607b..f1f8fef62f1f8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/default_allocation_notice.tsx @@ -8,9 +8,9 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; import { EuiCallOut } from '@elastic/eui'; -import { PhaseWithAllocation, DataTierRole } from '../../../../../../common/types'; +import { PhaseWithAllocation, DataTierRole } from '../../../../../../../../../common/types'; -import { AllocationNodeRole } from '../../../../lib'; +import { AllocationNodeRole } from '../../../../../../../lib'; const i18nTextsNodeRoleToDataTier: Record = { data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/index.ts similarity index 99% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/index.ts index 937e3dd28da97..0782782e77fad 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/index.ts @@ -5,9 +5,15 @@ */ export { NodesDataProvider } from './node_data_provider'; + export { NodeAllocation } from './node_allocation'; + export { NodeAttrsDetails } from './node_attrs_details'; + export { DataTierAllocation } from './data_tier_allocation'; + export { DefaultAllocationNotice } from './default_allocation_notice'; + export { NoNodeAttributesWarning } from './no_node_attributes_warning'; + export { CloudDataTierCallout } from './cloud_data_tier_callout'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx similarity index 86% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx index 69185277f64ce..56a59270b18af 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent } from 'react'; import { EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { PhaseWithAllocation } from '../../../../../../common/types'; +import { PhaseWithAllocation } from '../../../../../../../../../common/types'; const i18nTexts = { title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel', { @@ -19,7 +19,7 @@ const i18nTexts = { 'xpack.indexLifecycleMgmt.editPolicy.warm.nodeAttributesMissingDescription', { defaultMessage: - 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Warm nodes will be used instead.', + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.', } ), }, @@ -28,7 +28,7 @@ const i18nTexts = { 'xpack.indexLifecycleMgmt.editPolicy.cold.nodeAttributesMissingDescription', { defaultMessage: - 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation. Cold nodes will be used instead.', + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.', } ), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx similarity index 68% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx index a57a6ba4ff2c6..407bb9ea92e85 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx @@ -5,18 +5,22 @@ */ import React, { useState, FunctionComponent } from 'react'; +import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiSelect, EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui'; -import { PhaseWithAllocationAction } from '../../../../../../common/types'; -import { propertyof } from '../../../../services/policies/policy_validation'; +import { PhaseWithAllocationAction } from '../../../../../../../../../common/types'; -import { ErrableFormRow } from '../form_errors'; +import { UseField, SelectField, useFormData } from '../../../../../../../../shared_imports'; + +import { propertyof } from '../../../../../../../services/policies/policy_validation'; + +import { LearnMoreLink } from '../../../../learn_more_link'; import { NodeAttrsDetails } from './node_attrs_details'; + import { SharedProps } from './types'; -import { LearnMoreLink } from '../learn_more_link'; const learnMoreLink = ( = ({ - phase, - setPhaseData, - errors, - phaseData, - isShowingErrors, - nodes, -}) => { +export const NodeAllocation: FunctionComponent = ({ phase, nodes }) => { + const allocationNodeAttributePath = `_meta.${phase}.allocationNodeAttribute`; + + const [formData] = useFormData({ + watch: [allocationNodeAttributePath], + }); + + const selectedAllocationNodeAttribute = get(formData, allocationNodeAttributePath); + const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( null ); @@ -76,40 +81,33 @@ export const NodeAllocation: FunctionComponent = ({ TODO: this field component must be revisited to support setting multiple require values and to support setting `include and exclude values on ILM policies. See https://github.com/elastic/kibana/issues/77344 */} - setSelectedNodeAttrsForDetails(phaseData.selectedNodeAttrs)} + onClick={() => setSelectedNodeAttrsForDetails(selectedAllocationNodeAttribute)} > - ) : null - } - > - { - setPhaseData(nodeAttrsProperty, e.target.value); - }} - /> - - + ) : undefined, + euiFieldProps: { + 'data-test-subj': `${phase}-${nodeAttrsProperty}`, + options: [{ text: i18nTexts.doNotModifyAllocationOption, value: '' }].concat( + nodeOptions + ), + hasNoInitialSelection: false, + }, + }} + /> {selectedNodeAttrsForDetails ? ( void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_data_provider.tsx similarity index 92% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_data_provider.tsx index a7c0f3ec7c866..bb01a42e378fa 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/node_data_provider.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_data_provider.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ListNodesRouteResponse } from '../../../../../../common/types'; -import { useLoadNodes } from '../../../../services/api'; +import { ListNodesRouteResponse } from '../../../../../../../../../common/types'; +import { useLoadNodes } from '../../../../../../../services/api'; interface Props { children: (data: ListNodesRouteResponse) => JSX.Element; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/types.ts similarity index 66% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/types.ts index d3dd536d97df0..7bd620757a8ac 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/types.ts @@ -7,16 +7,10 @@ import { ListNodesRouteResponse, PhaseWithAllocation, - PhaseWithAllocationAction, -} from '../../../../../../common/types'; -import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; +} from '../../../../../../../../../common/types'; export interface SharedProps { phase: PhaseWithAllocation; - errors?: PhaseValidationErrors; - phaseData: PhaseWithAllocationAction; - setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; - isShowingErrors: boolean; nodes: ListNodesRouteResponse['nodesByAttributes']; hasNodeAttributes: boolean; /** diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/data_tier_allocation_field.tsx similarity index 74% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/data_tier_allocation_field.tsx index abed1bd3a8482..73814537ff276 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -4,15 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { get } from 'lodash'; import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; + import { EuiDescribedFormGroup, EuiFormRow, EuiSpacer } from '@elastic/eui'; -import { useKibana } from '../../../../../../shared_imports'; -import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../../common/types'; -import { PhaseValidationErrors } from '../../../../../services/policies/policy_validation'; -import { getAvailableNodeRoleForPhase } from '../../../../../lib/data_tiers'; -import { isNodeRoleFirstPreference } from '../../../../../lib/data_tiers/is_node_role_first_preference'; +import { useKibana, useFormData } from '../../../../../../../shared_imports'; + +import { PhaseWithAllocation } from '../../../../../../../../common/types'; + +import { getAvailableNodeRoleForPhase } from '../../../../../../lib/data_tiers'; + +import { isNodeRoleFirstPreference } from '../../../../../../lib'; + +import { DataTierAllocationType } from '../../../../types'; import { DataTierAllocation, @@ -20,7 +26,7 @@ import { NoNodeAttributesWarning, NodesDataProvider, CloudDataTierCallout, -} from '../../data_tier_allocation'; +} from './components'; const i18nTexts = { title: i18n.translate('xpack.indexLifecycleMgmt.common.dataTier.title', { @@ -29,29 +35,22 @@ const i18nTexts = { }; interface Props { - description: React.ReactNode; phase: PhaseWithAllocation; - setPhaseData: (dataKey: keyof PhaseWithAllocationAction, value: string) => void; - isShowingErrors: boolean; - errors?: PhaseValidationErrors; - phaseData: PhaseWithAllocationAction; + description: React.ReactNode; } /** * Top-level layout control for the data tier allocation field. */ -export const DataTierAllocationField: FunctionComponent = ({ - description, - phase, - phaseData, - setPhaseData, - isShowingErrors, - errors, -}) => { +export const DataTierAllocationField: FunctionComponent = ({ phase, description }) => { const { services: { cloud }, } = useKibana(); + const dataTierAllocationTypePath = `_meta.${phase}.dataTierAllocationType`; + const [formData] = useFormData({ watch: dataTierAllocationTypePath }); + const allocationType: DataTierAllocationType = get(formData, dataTierAllocationTypePath); + return ( {({ nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig }) => { @@ -60,11 +59,11 @@ export const DataTierAllocationField: FunctionComponent = ({ nodeRole.trim().startsWith('data_') ); const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); + const isCloudEnabled = cloud?.isCloudEnabled ?? false; const renderNotice = () => { - switch (phaseData.dataTierAllocationType) { - case 'default': - const isCloudEnabled = cloud?.isCloudEnabled ?? false; + switch (allocationType) { + case 'node_roles': if (isCloudEnabled && phase === 'cold') { const isUsingNodeRolesAllocation = !isUsingDeprecatedDataRoleConfig && hasDataNodeRoles; @@ -94,7 +93,7 @@ export const DataTierAllocationField: FunctionComponent = ({ ); } break; - case 'custom': + case 'node_attrs': if (!hasNodeAttrs) { return ( <> @@ -120,13 +119,9 @@ export const DataTierAllocationField: FunctionComponent = ({ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/index.ts similarity index 74% rename from x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/index.ts index fe97b85778a53..f9e939058adb9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { serializePhaseWithAllocation } from './serialize_phase_with_allocation'; +export { DataTierAllocationField } from './data_tier_allocation_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx index c6f02fd219130..b410bd0e6b3b0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx @@ -3,33 +3,32 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiDescribedFormGroup, EuiSpacer, EuiTextColor } from '@elastic/eui'; -import React from 'react'; -import { Phases } from '../../../../../../../common/types'; +import React, { useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiTextColor } from '@elastic/eui'; -import { UseField, ToggleField, NumericField, useFormData } from '../../../../../../shared_imports'; +import { UseField, ToggleField, NumericField } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; -import { LearnMoreLink } from '../../'; +import { useEditPolicyContext } from '../../../edit_policy_context'; + +import { LearnMoreLink, DescribedFormField } from '../../'; interface Props { - phase: keyof Phases & string; + phase: 'hot' | 'warm'; } -const forceMergeEnabledPath = '_meta.hot.forceMergeEnabled'; - export const Forcemerge: React.FunctionComponent = ({ phase }) => { - const [formData] = useFormData({ - watch: forceMergeEnabledPath, - }); - const forceMergeEnabled = get(formData, forceMergeEnabledPath); + const { originalPolicy } = useEditPolicyContext(); + + const initialToggleValue = useMemo(() => { + return Boolean(originalPolicy.phases[phase]?.actions?.forcemerge); + }, [originalPolicy, phase]); return ( - = ({ phase }) => { } titleSize="xs" fullWidth + switchProps={{ + 'aria-label': i18nTexts.editPolicy.forceMergeEnabledFieldLabel, + 'data-test-subj': `${phase}-forceMergeSwitch`, + 'aria-controls': 'forcemergeContent', + label: i18nTexts.editPolicy.forceMergeEnabledFieldLabel, + initialValue: initialToggleValue, + }} > -
- {forceMergeEnabled && ( - <> - - - - )} + +
-
+ ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts index 3b94d36a977d1..6355dab89771d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/index.ts @@ -11,3 +11,5 @@ export { DataTierAllocationField } from './data_tier_allocation_field'; export { Forcemerge } from './forcemerge_field'; export { SetPriorityInput } from './set_priority_input'; + +export { MinAgeInputField } from './min_age_input_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/min_age_input_field/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/min_age_input_field/index.ts new file mode 100644 index 0000000000000..0228a524f8129 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/min_age_input_field/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MinAgeInputField } from './min_age_input_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/min_age_input_field/min_age_input_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/min_age_input_field/min_age_input_field.tsx new file mode 100644 index 0000000000000..f37c387354418 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/min_age_input_field/min_age_input_field.tsx @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent } from 'react'; +import { get } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { + useFormData, + UseField, + NumericField, + SelectField, +} from '../../../../../../../shared_imports'; + +import { LearnMoreLink } from '../../../learn_more_link'; +import { useRolloverPath } from '../../../../constants'; + +import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util'; + +type PhaseWithMinAgeAction = 'warm' | 'cold' | 'delete'; + +interface Props { + phase: PhaseWithMinAgeAction; +} + +export const MinAgeInputField: FunctionComponent = ({ phase }): React.ReactElement => { + const [formData] = useFormData({ watch: useRolloverPath }); + const rolloverEnabled = get(formData, useRolloverPath); + + let daysOptionLabel; + let hoursOptionLabel; + let minutesOptionLabel; + let secondsOptionLabel; + let millisecondsOptionLabel; + let microsecondsOptionLabel; + let nanosecondsOptionLabel; + + if (rolloverEnabled) { + daysOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel', + { + defaultMessage: 'days from rollover', + } + ); + + hoursOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel', + { + defaultMessage: 'hours from rollover', + } + ); + minutesOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.rolloverMinutesOptionLabel', + { + defaultMessage: 'minutes from rollover', + } + ); + + secondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.rolloverSecondsOptionLabel', + { + defaultMessage: 'seconds from rollover', + } + ); + millisecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.rolloverMilliSecondsOptionLabel', + { + defaultMessage: 'milliseconds from rollover', + } + ); + + microsecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel', + { + defaultMessage: 'microseconds from rollover', + } + ); + + nanosecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.rolloverNanoSecondsOptionLabel', + { + defaultMessage: 'nanoseconds from rollover', + } + ); + } else { + daysOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationDaysOptionLabel', + { + defaultMessage: 'days from index creation', + } + ); + + hoursOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationHoursOptionLabel', + { + defaultMessage: 'hours from index creation', + } + ); + + minutesOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel', + { + defaultMessage: 'minutes from index creation', + } + ); + + secondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel', + { + defaultMessage: 'seconds from index creation', + } + ); + + millisecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationMilliSecondsOptionLabel', + { + defaultMessage: 'milliseconds from index creation', + } + ); + + microsecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationMicroSecondsOptionLabel', + { + defaultMessage: 'microseconds from index creation', + } + ); + + nanosecondsOptionLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel', + { + defaultMessage: 'nanoseconds from index creation', + } + ); + } + + return ( + + + + } + /> + ), + euiFieldProps: { + 'data-test-subj': `${phase}-selectedMinimumAge`, + min: 0, + }, + }} + /> + + + + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/min_age_input_field/util.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/min_age_input_field/util.ts new file mode 100644 index 0000000000000..181894badba7b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/min_age_input_field/util.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { Phases } from '../../../../../../../../common/types'; + +type PhaseWithMinAgeAction = 'warm' | 'cold' | 'delete'; + +export function getUnitsAriaLabelForPhase(phase: keyof Phases) { + // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. + switch (phase) { + case 'warm': + return i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel', + { + defaultMessage: 'Units for timing of warm phase', + } + ); + + case 'cold': + return i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel', + { + defaultMessage: 'Units for timing of cold phase', + } + ); + + case 'delete': + return i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel', + { + defaultMessage: 'Units for timing of delete phase', + } + ); + } +} +export function getTimingLabelForPhase(phase: PhaseWithMinAgeAction) { + // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. + switch (phase) { + case 'warm': + return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel', { + defaultMessage: 'Timing for warm phase', + }); + + case 'cold': + return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel', { + defaultMessage: 'Timing for cold phase', + }); + + case 'delete': + return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', { + defaultMessage: 'Timing for delete phase', + }); + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase.tsx deleted file mode 100644 index d0aaec367104e..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase.tsx +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, FunctionComponent } from 'react'; -import { get } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiTextColor, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiFormRow, - EuiFieldNumber, - EuiSwitch, - EuiDescribedFormGroup, -} from '@elastic/eui'; - -import { useFormData } from '../../../../../shared_imports'; -import { Phases, WarmPhase as WarmPhaseInterface } from '../../../../../../common/types'; -import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; - -import { useRolloverPath } from './shared'; - -import { - LearnMoreLink, - ActiveBadge, - PhaseErrorMessage, - OptionalLabel, - ErrableFormRow, - SetPriorityInput, - MinAgeInput, - DescribedFormField, - Forcemerge, -} from '../'; - -import { DataTierAllocationField } from './shared'; - -const i18nTexts = { - shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { - defaultMessage: 'Shrink index', - }), - moveToWarmPhaseOnRolloverLabel: i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', - { - defaultMessage: 'Move to warm phase on rollover', - } - ), - dataTierAllocation: { - description: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.description', { - defaultMessage: 'Move data to nodes optimized for less-frequent, read-only access.', - }), - }, -}; - -const warmProperty: keyof Phases = 'warm'; -const phaseProperty = (propertyName: keyof WarmPhaseInterface) => propertyName; - -interface Props { - setPhaseData: ( - key: keyof WarmPhaseInterface & string, - value: boolean | string | undefined - ) => void; - phaseData: WarmPhaseInterface; - isShowingErrors: boolean; - errors?: PhaseValidationErrors; -} -export const WarmPhase: FunctionComponent = ({ - setPhaseData, - phaseData, - errors, - isShowingErrors, -}) => { - const [formData] = useFormData({ - watch: useRolloverPath, - }); - - const hotPhaseRolloverEnabled = get(formData, useRolloverPath); - - return ( -
- <> - -

- -

{' '} - {phaseData.phaseEnabled && !isShowingErrors ? : null} - -
- } - titleSize="s" - description={ - -

- -

- - } - id={`${warmProperty}-${phaseProperty('phaseEnabled')}`} - checked={phaseData.phaseEnabled} - onChange={(e) => { - setPhaseData(phaseProperty('phaseEnabled'), e.target.checked); - }} - aria-controls="warmPhaseContent" - /> -
- } - fullWidth - > - - {phaseData.phaseEnabled ? ( - - {hotPhaseRolloverEnabled ? ( - - { - setPhaseData(phaseProperty('warmPhaseOnRollover'), e.target.checked); - }} - /> - - ) : null} - {!phaseData.warmPhaseOnRollover || !hotPhaseRolloverEnabled ? ( - - - - errors={errors} - phaseData={phaseData} - phase={warmProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - rolloverEnabled={hotPhaseRolloverEnabled} - /> - - ) : null} - - ) : null} - -
- - {phaseData.phaseEnabled ? ( - - {/* Data tier allocation section */} - - - - {i18n.translate('xpack.indexLifecycleMgmt.warmPhase.replicasTitle', { - defaultMessage: 'Replicas', - })} - - } - description={i18n.translate( - 'xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription', - { - defaultMessage: - 'Set the number of replicas. Remains the same as the previous phase by default.', - } - )} - switchProps={{ - label: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel', - { defaultMessage: 'Set replicas' } - ), - initialValue: Boolean(phaseData.selectedReplicaCount), - onChange: (v) => { - if (!v) { - setPhaseData('selectedReplicaCount', ''); - } - }, - }} - fullWidth - > - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.selectedReplicaCount} - > - { - setPhaseData('selectedReplicaCount', e.target.value); - }} - min={0} - /> - - - - - - } - description={ - - {' '} - - - } - fullWidth - titleSize="xs" - > - - { - setPhaseData(phaseProperty('shrinkEnabled'), e.target.checked); - }} - label={i18nTexts.shrinkLabel} - aria-label={i18nTexts.shrinkLabel} - aria-controls="shrinkContent" - /> - -
- {phaseData.shrinkEnabled ? ( - - - - - - { - setPhaseData( - phaseProperty('selectedPrimaryShardCount'), - e.target.value - ); - }} - min={1} - /> - - - - - - ) : null} -
-
-
- - - errors={errors} - phaseData={phaseData} - phase={warmProperty} - isShowingErrors={isShowingErrors} - setPhaseData={setPhaseData} - /> - - ) : null} - -
- ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/index.ts new file mode 100644 index 0000000000000..d0686270ba559 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { WarmPhase } from './warm_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx new file mode 100644 index 0000000000000..06c16e8bdd5ab --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; + +import { + EuiTextColor, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiDescribedFormGroup, +} from '@elastic/eui'; + +import { + useFormData, + UseField, + ToggleField, + useFormContext, + NumericField, +} from '../../../../../../shared_imports'; + +import { Phases } from '../../../../../../../common/types'; + +import { useRolloverPath, MinAgeInputField, Forcemerge, SetPriorityInput } from '../shared'; + +import { useEditPolicyContext } from '../../../edit_policy_context'; + +import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, DescribedFormField } from '../../'; + +import { DataTierAllocationField } from '../shared'; + +const i18nTexts = { + shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { + defaultMessage: 'Shrink index', + }), + dataTierAllocation: { + description: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.dataTier.description', { + defaultMessage: 'Move data to nodes optimized for less-frequent, read-only access.', + }), + }, +}; + +const warmProperty: keyof Phases = 'warm'; + +const formFieldPaths = { + enabled: '_meta.warm.enabled', + warmPhaseOnRollover: '_meta.warm.warmPhaseOnRollover', +}; + +export const WarmPhase: FunctionComponent = () => { + const { originalPolicy } = useEditPolicyContext(); + const form = useFormContext(); + const [formData] = useFormData({ + watch: [useRolloverPath, formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover], + }); + + const enabled = get(formData, formFieldPaths.enabled); + const hotPhaseRolloverEnabled = get(formData, useRolloverPath); + const warmPhaseOnRollover = get(formData, formFieldPaths.warmPhaseOnRollover); + const isShowingErrors = form.isValid === false; + + return ( +
+ <> + +

+ +

{' '} + {enabled && !isShowingErrors ? : null} + +
+ } + titleSize="s" + description={ + +

+ +

+ +
+ } + fullWidth + > + + {enabled && ( + + {hotPhaseRolloverEnabled && ( + + )} + {(!warmPhaseOnRollover || !hotPhaseRolloverEnabled) && ( + <> + + + + )} + + )} + + + + {enabled && ( + + {/* Data tier allocation section */} + + + + {i18n.translate('xpack.indexLifecycleMgmt.warmPhase.replicasTitle', { + defaultMessage: 'Replicas', + })} + + } + description={i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription', + { + defaultMessage: + 'Set the number of replicas. Remains the same as the previous phase by default.', + } + )} + switchProps={{ + 'data-test-subj': 'warm-setReplicasSwitch', + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel', + { defaultMessage: 'Set replicas' } + ), + initialValue: Boolean( + originalPolicy.phases.warm?.actions?.allocate?.number_of_replicas + ), + }} + fullWidth + > + + + + + + } + description={ + + {' '} + + + } + titleSize="xs" + switchProps={{ + 'aria-controls': 'shrinkContent', + 'data-test-subj': 'shrinkSwitch', + label: i18nTexts.shrinkLabel, + 'aria-label': i18nTexts.shrinkLabel, + initialValue: Boolean(originalPolicy.phases.warm?.actions?.shrink), + }} + fullWidth + > +
+ + + + + + + +
+
+ + + + +
+ )} + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx index e9ce193118b35..7098b018d6dfd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -25,6 +25,7 @@ import { import { SerializedPolicy } from '../../../../../common/types'; import { useFormContext, useFormData } from '../../../../shared_imports'; +import { FormInternal } from '../types'; interface Props { legacyPolicy: SerializedPolicy; @@ -44,27 +45,30 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({ */ const [policy, setPolicy] = useState(undefined); - const form = useFormContext(); - const [formData, getFormData] = useFormData(); + const { validate: validateForm } = useFormContext(); + const [, getFormData] = useFormData(); + + const updatePolicy = useCallback(async () => { + setPolicy(undefined); + if (await validateForm()) { + const p = getFormData() as SerializedPolicy; + setPolicy({ + ...legacyPolicy, + phases: { + ...legacyPolicy.phases, + hot: p.phases.hot, + warm: p.phases.warm, + cold: p.phases.cold, + }, + }); + } else { + setPolicy(null); + } + }, [setPolicy, getFormData, legacyPolicy, validateForm]); useEffect(() => { - (async function checkPolicy() { - setPolicy(undefined); - if (await form.validate()) { - const p = getFormData() as SerializedPolicy; - setPolicy({ - ...legacyPolicy, - phases: { - ...legacyPolicy.phases, - hot: p.phases.hot, - }, - }); - } else { - setPolicy(null); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form, legacyPolicy, formData]); + updatePolicy(); + }, [updatePolicy]); let content: React.ReactNode; switch (policy) { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx deleted file mode 100644 index 5efbfabdf093d..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * PLEASE NOTE: This component is currently duplicated. A version of this component wired up with - * the form lib lives in ./phases/shared - */ - -import React, { Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; - -import { LearnMoreLink } from './'; -import { OptionalLabel } from './'; -import { ErrableFormRow } from './'; -import { PhaseWithIndexPriority, Phases } from '../../../../../common/types'; -import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; - -interface Props { - errors?: PhaseValidationErrors; - phase: keyof Phases & string; - phaseData: T; - setPhaseData: (dataKey: keyof T & string, value: any) => void; - isShowingErrors: boolean; -} -export const SetPriorityInput = ({ - errors, - phaseData, - phase, - setPhaseData, - isShowingErrors, -}: React.PropsWithChildren>) => { - const phaseIndexPriorityProperty = propertyof('phaseIndexPriority'); - return ( - - - - } - description={ - - {' '} - - - } - titleSize="xs" - fullWidth - > - - - - - } - isShowingErrors={isShowingErrors} - errors={errors?.phaseIndexPriority} - > - { - setPhaseData(phaseIndexPriorityProperty, e.target.value); - }} - min={0} - /> - - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx index ff4301808db33..d188a172d746b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/toggleable_field.tsx @@ -9,7 +9,7 @@ import { EuiSpacer, EuiSwitch, EuiSwitchProps } from '@elastic/eui'; export interface Props extends Omit { initialValue: boolean; - onChange: (nextValue: boolean) => void; + onChange?: (nextValue: boolean) => void; } export const ToggleableField: FunctionComponent = ({ @@ -28,7 +28,9 @@ export const ToggleableField: FunctionComponent = ({ onChange={(e) => { const nextValue = e.target.checked; setIsContentVisible(nextValue); - onChange(nextValue); + if (onChange) { + onChange(nextValue); + } }} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts index bb24eea64ec8c..f0294a5391d21 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts @@ -10,22 +10,41 @@ import { SerializedPolicy } from '../../../../common/types'; import { splitSizeAndUnits } from '../../services/policies/policy_serialization'; +import { determineDataTierAllocationType } from '../../lib'; + import { FormInternal } from './types'; -export const deserializer = (policy: SerializedPolicy): FormInternal => - produce( +export const deserializer = (policy: SerializedPolicy): FormInternal => { + const { + phases: { hot, warm, cold }, + } = policy; + + const _meta: FormInternal['_meta'] = { + hot: { + useRollover: Boolean(hot?.actions?.rollover), + forceMergeEnabled: Boolean(hot?.actions?.forcemerge), + bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', + }, + warm: { + enabled: Boolean(warm), + warmPhaseOnRollover: Boolean(warm?.min_age === '0ms'), + forceMergeEnabled: Boolean(warm?.actions?.forcemerge), + bestCompression: warm?.actions?.forcemerge?.index_codec === 'best_compression', + dataTierAllocationType: determineDataTierAllocationType(warm?.actions), + }, + cold: { + enabled: Boolean(cold), + dataTierAllocationType: determineDataTierAllocationType(cold?.actions), + freezeEnabled: Boolean(cold?.actions?.freeze), + }, + }; + + return produce( { ...policy, - _meta: { - hot: { - useRollover: Boolean(policy.phases.hot?.actions?.rollover), - forceMergeEnabled: Boolean(policy.phases.hot?.actions?.forcemerge), - bestCompression: - policy.phases.hot?.actions?.forcemerge?.index_codec === 'best_compression', - }, - }, + _meta, }, - (draft) => { + (draft: FormInternal) => { if (draft.phases.hot?.actions?.rollover) { if (draft.phases.hot.actions.rollover.max_size) { const maxSize = splitSizeAndUnits(draft.phases.hot.actions.rollover.max_size); @@ -39,5 +58,34 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => draft._meta.hot.maxAgeUnit = maxAge.units; } } + + if (draft.phases.warm) { + if (draft.phases.warm.actions?.allocate?.require) { + Object.entries(draft.phases.warm.actions.allocate.require).forEach((entry) => { + draft._meta.warm.allocationNodeAttribute = entry.join(':'); + }); + } + + if (draft.phases.warm.min_age) { + const minAge = splitSizeAndUnits(draft.phases.warm.min_age); + draft.phases.warm.min_age = minAge.size; + draft._meta.warm.minAgeUnit = minAge.units; + } + } + + if (draft.phases.cold) { + if (draft.phases.cold.actions?.allocate?.require) { + Object.entries(draft.phases.cold.actions.allocate.require).forEach((entry) => { + draft._meta.cold.allocationNodeAttribute = entry.join(':'); + }); + } + + if (draft.phases.cold.min_age) { + const minAge = splitSizeAndUnits(draft.phases.cold.min_age); + draft.phases.cold.min_age = minAge.size; + draft._meta.cold.minAgeUnit = minAge.units; + } + } } ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 8f8b0447f378a..5397f5da2d6bb 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -67,6 +67,8 @@ import { schema } from './form_schema'; import { deserializer } from './deserializer'; import { createSerializer } from './serializer'; +import { EditPolicyContextProvider } from './edit_policy_context'; + export interface Props { policies: PolicyFromES[]; policyName: string; @@ -89,6 +91,8 @@ const mergeAllSerializedPolicies = ( phases: { ...legacySerializedPolicy.phases, hot: serializedPolicy.phases.hot, + warm: serializedPolicy.phases.warm, + cold: serializedPolicy.phases.cold, }, }; }; @@ -113,9 +117,11 @@ export const EditPolicy: React.FunctionComponent = ({ return createSerializer(existingPolicy?.policy); }, [existingPolicy?.policy]); + const originalPolicy = existingPolicy?.policy ?? defaultPolicy; + const { form } = useForm({ schema, - defaultValue: existingPolicy?.policy ?? defaultPolicy, + defaultValue: originalPolicy, deserializer, serializer, }); @@ -132,22 +138,6 @@ export const EditPolicy: React.FunctionComponent = ({ history.push('/policies'); }; - const setWarmPhaseOnRollover = useCallback( - (value: boolean) => { - setPolicy((p) => ({ - ...p, - phases: { - ...p.phases, - warm: { - ...p.phases.warm, - warmPhaseOnRollover: value, - }, - }, - })); - }, - [setPolicy] - ); - const submit = async () => { setIsShowingErrors(true); const { data: formLibPolicy, isValid: newIsValid } = await form.submit(); @@ -206,248 +196,235 @@ export const EditPolicy: React.FunctionComponent = ({ [setPolicy] ); - const setWarmPhaseData = useCallback( - (key: string, value: any) => setPhaseData('warm', key, value), - [setPhaseData] - ); - const setColdPhaseData = useCallback( - (key: string, value: any) => setPhaseData('cold', key, value), - [setPhaseData] - ); const setDeletePhaseData = useCallback( (key: string, value: any) => setPhaseData('delete', key, value), [setPhaseData] ); return ( - - - - -

- {isNewPolicy - ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { - defaultMessage: 'Create an index lifecycle policy', - }) - : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { - defaultMessage: 'Edit index lifecycle policy {originalPolicyName}', - values: { originalPolicyName }, - })} -

-
- -
-
- - -

- + +

+ {isNewPolicy + ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', { + defaultMessage: 'Create an index lifecycle policy', + }) + : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', { + defaultMessage: 'Edit index lifecycle policy {originalPolicyName}', + values: { originalPolicyName }, + })} +

+ + +
+ + + +

+ {' '} - - } - /> -

-
+ />{' '} + + } + /> +

+ - + - {isNewPolicy ? null : ( - - -

- + {isNewPolicy ? null : ( + + +

+ + + + .{' '} - - .{' '} - +

+
+ + + + { + setSaveAsNew(e.target.checked); + }} + label={ + + + + } /> -

- - - - - { - setSaveAsNew(e.target.checked); - }} - label={ - + +
+ )} + + {saveAsNew || isNewPolicy ? ( + + - } - /> - - - )} - - {saveAsNew || isNewPolicy ? ( - - +
+ } + titleSize="s" + fullWidth + > + - -
- } - titleSize="s" - fullWidth - > - + { + setPolicy({ ...policy, name: e.target.value }); + }} /> - } - > - { - setPolicy({ ...policy, name: e.target.value }); - }} - /> - - - ) : null} - - - - - - - - 0} - setPhaseData={setWarmPhaseData} - phaseData={policy.phases.warm} - /> - - - - 0} - setPhaseData={setColdPhaseData} - phaseData={policy.phases.cold} - /> - - - - 0 - } - getUrlForApp={getUrlForApp} - setPhaseData={setDeletePhaseData} - phaseData={policy.phases.delete} - /> - - - - - - - - - {saveAsNew ? ( - - ) : ( + + + ) : null} + + + + + + + + + + + + + + + + 0 + } + getUrlForApp={getUrlForApp} + setPhaseData={setDeletePhaseData} + phaseData={policy.phases.delete} + /> + + + + + + + + + {saveAsNew ? ( + + ) : ( + + )} + + + + + - )} - - - - - + + + + + + + + {isShowingPolicyJsonFlyout ? ( - - - - - - - - {isShowingPolicyJsonFlyout ? ( - - ) : ( - - )} - - - - - {isShowingPolicyJsonFlyout ? ( - setIsShowingPolicyJsonFlyout(false)} - /> - ) : null} - -
- - - + ) : ( + + )} + + + + + {isShowingPolicyJsonFlyout ? ( + setIsShowingPolicyJsonFlyout(false)} + /> + ) : null} + +
+ + + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx new file mode 100644 index 0000000000000..4748c26d6cec1 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { createContext, ReactChild, useContext } from 'react'; +import { SerializedPolicy } from '../../../../common/types'; + +interface EditPolicyContextValue { + originalPolicy: SerializedPolicy; +} + +const EditPolicyContext = createContext(null as any); + +export const EditPolicyContextProvider = ({ + value, + children, +}: { + value: EditPolicyContextValue; + children: ReactChild; +}) => { + return {children}; +}; + +export const useEditPolicyContext = () => { + const ctx = useContext(EditPolicyContext); + if (!ctx) { + throw new Error('useEditPolicyContext can only be called inside of EditPolicyContext!'); + } + return ctx; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts index 806164c8b0da1..070f03f74b954 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts @@ -7,13 +7,23 @@ import { i18n } from '@kbn/i18n'; import { FormSchema, fieldValidators } from '../../../shared_imports'; -import { defaultSetPriority } from '../../constants'; +import { defaultSetPriority, defaultPhaseIndexPriority } from '../../constants'; import { FormInternal } from './types'; -import { ifExistsNumberGreaterThanZero, rolloverThresholdsValidator } from './form_validations'; + +import { + ifExistsNumberGreaterThanZero, + ifExistsNumberNonNegative, + rolloverThresholdsValidator, +} from './form_validations'; + import { i18nTexts } from './i18n_texts'; -const { emptyField } = fieldValidators; +const { emptyField, numberGreaterThanField } = fieldValidators; + +const serializers = { + stringToNumber: (v: string): any => (v ? parseInt(v, 10) : undefined), +}; export const schema: FormSchema = { _meta: { @@ -30,21 +40,62 @@ export const schema: FormSchema = { maxAgeUnit: { defaultValue: 'd', }, - forceMergeEnabled: { - label: i18nTexts.editPolicy.forceMergeEnabledFieldLabel, - }, bestCompression: { - label: i18n.translate('xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel', { - defaultMessage: 'Compress stored fields', + label: i18nTexts.editPolicy.bestCompressionFieldLabel, + helpText: i18nTexts.editPolicy.bestCompressionFieldHelpText, + }, + }, + warm: { + enabled: { + defaultValue: false, + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.warmPhase.activateWarmPhaseSwitchLabel', + { defaultMessage: 'Activate warm phase' } + ), + }, + warmPhaseOnRollover: { + defaultValue: true, + label: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel', { + defaultMessage: 'Move to warm phase on rollover', }), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.forceMerge.bestCompressionText', - { - defaultMessage: - 'Use higher compression for stored fields at the cost of slower performance.', - } + }, + minAgeUnit: { + defaultValue: 'ms', + }, + bestCompression: { + label: i18nTexts.editPolicy.bestCompressionFieldLabel, + helpText: i18nTexts.editPolicy.bestCompressionFieldHelpText, + }, + dataTierAllocationType: { + label: i18nTexts.editPolicy.allocationTypeOptionsFieldLabel, + }, + allocationNodeAttribute: { + label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel, + }, + }, + cold: { + enabled: { + defaultValue: false, + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel', + { defaultMessage: 'Activate cold phase' } ), }, + freezeEnabled: { + defaultValue: false, + label: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', + }), + }, + minAgeUnit: { + defaultValue: 'd', + }, + dataTierAllocationType: { + label: i18nTexts.editPolicy.allocationTypeOptionsFieldLabel, + }, + allocationNodeAttribute: { + label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel, + }, }, }, phases: { @@ -76,7 +127,7 @@ export const schema: FormSchema = { validator: ifExistsNumberGreaterThanZero, }, ], - serializer: (v: string): any => (v ? parseInt(v, 10) : undefined), + serializer: serializers.stringToNumber, }, max_size: { label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.maximumIndexSizeLabel', { @@ -94,9 +145,7 @@ export const schema: FormSchema = { }, forcemerge: { max_num_segments: { - label: i18n.translate('xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel', { - defaultMessage: 'Number of segments', - }), + label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel, validations: [ { validator: emptyField( @@ -110,17 +159,135 @@ export const schema: FormSchema = { validator: ifExistsNumberGreaterThanZero, }, ], - serializer: (v: string): any => (v ? parseInt(v, 10) : undefined), + serializer: serializers.stringToNumber, }, }, set_priority: { priority: { defaultValue: defaultSetPriority as any, - label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.indexPriorityLabel', { - defaultMessage: 'Index priority (optional)', + label: i18nTexts.editPolicy.setPriorityFieldLabel, + validations: [{ validator: ifExistsNumberNonNegative }], + serializer: serializers.stringToNumber, + }, + }, + }, + }, + warm: { + min_age: { + defaultValue: '0', + validations: [ + { + validator: (arg) => + numberGreaterThanField({ + than: 0, + allowEquality: true, + message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired, + })({ + ...arg, + value: arg.value === '' ? -Infinity : parseInt(arg.value, 10), + }), + }, + ], + }, + actions: { + allocate: { + number_of_replicas: { + label: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel', { + defaultMessage: 'Number of replicas (optional)', + }), + validations: [ + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + serializer: serializers.stringToNumber, + }, + }, + shrink: { + number_of_shards: { + label: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel', { + defaultMessage: 'Number of primary shards', + }), + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { + validator: numberGreaterThanField({ + message: i18nTexts.editPolicy.errors.numberGreatThan0Required, + than: 0, + }), + }, + ], + serializer: serializers.stringToNumber, + }, + }, + forcemerge: { + max_num_segments: { + label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError', + { defaultMessage: 'A value for number of segments is required.' } + ) + ), + }, + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + serializer: serializers.stringToNumber, + }, + }, + set_priority: { + priority: { + defaultValue: defaultPhaseIndexPriority as any, + label: i18nTexts.editPolicy.setPriorityFieldLabel, + validations: [{ validator: ifExistsNumberNonNegative }], + serializer: serializers.stringToNumber, + }, + }, + }, + }, + cold: { + min_age: { + defaultValue: '0', + validations: [ + { + validator: (arg) => + numberGreaterThanField({ + than: 0, + allowEquality: true, + message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired, + })({ + ...arg, + value: arg.value === '' ? -Infinity : parseInt(arg.value, 10), + }), + }, + ], + }, + actions: { + allocate: { + number_of_replicas: { + label: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel', { + defaultMessage: 'Number of replicas (optional)', }), - validations: [{ validator: ifExistsNumberGreaterThanZero }], - serializer: (v: string): any => (v ? parseInt(v, 10) : undefined), + validations: [ + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + serializer: serializers.stringToNumber, + }, + }, + set_priority: { + priority: { + defaultValue: '0' as any, + label: i18nTexts.editPolicy.setPriorityFieldLabel, + validations: [{ validator: ifExistsNumberNonNegative }], + serializer: serializers.stringToNumber, }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts index b937ea2043138..9c855ccb41624 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts @@ -4,34 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { fieldValidators, ValidationFunc } from '../../../shared_imports'; -import { i18nTexts } from './components/phases/hot_phase/i18n_texts'; - import { ROLLOVER_FORM_PATHS } from './constants'; -const { numberGreaterThanField } = fieldValidators; +import { i18nTexts } from './i18n_texts'; -export const positiveNumberRequiredMessage = i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.numberAboveZeroRequiredError', - { - defaultMessage: 'Only numbers above 0 are allowed.', - } -); +const { numberGreaterThanField } = fieldValidators; -export const ifExistsNumberGreaterThanZero: ValidationFunc = (arg) => { - if (arg.value) { - return numberGreaterThanField({ - than: 0, - message: positiveNumberRequiredMessage, - })({ - ...arg, - value: parseInt(arg.value, 10), - }); - } +const createIfNumberExistsValidator = ({ + than, + message, +}: { + than: number; + message: string; +}): ValidationFunc => { + return (arg) => { + if (arg.value) { + return numberGreaterThanField({ + than, + message, + })({ + ...arg, + value: parseInt(arg.value, 10), + }); + } + }; }; +export const ifExistsNumberGreaterThanZero = createIfNumberExistsValidator({ + than: 0, + message: i18nTexts.editPolicy.errors.numberGreatThan0Required, +}); + +export const ifExistsNumberNonNegative = createIfNumberExistsValidator({ + than: -1, + message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired, +}); + /** * A special validation type used to keep track of validation errors for * the rollover threshold values not being set (e.g., age and doc count) @@ -54,16 +64,22 @@ export const rolloverThresholdsValidator: ValidationFunc = ({ form }) => { ) ) { fields[ROLLOVER_FORM_PATHS.maxAge].setErrors([ - { validationType: ROLLOVER_EMPTY_VALIDATION, message: i18nTexts.maximumAgeRequiredMessage }, + { + validationType: ROLLOVER_EMPTY_VALIDATION, + message: i18nTexts.editPolicy.errors.maximumAgeRequiredMessage, + }, ]); fields[ROLLOVER_FORM_PATHS.maxDocs].setErrors([ { validationType: ROLLOVER_EMPTY_VALIDATION, - message: i18nTexts.maximumDocumentsRequiredMessage, + message: i18nTexts.editPolicy.errors.maximumDocumentsRequiredMessage, }, ]); fields[ROLLOVER_FORM_PATHS.maxSize].setErrors([ - { validationType: ROLLOVER_EMPTY_VALIDATION, message: i18nTexts.maximumSizeRequiredMessage }, + { + validationType: ROLLOVER_EMPTY_VALIDATION, + message: i18nTexts.editPolicy.errors.maximumSizeRequiredMessage, + }, ]); } else { fields[ROLLOVER_FORM_PATHS.maxAge].clearErrors(ROLLOVER_EMPTY_VALIDATION); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 31bb10b402d27..1fba69b7634ae 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -11,5 +11,93 @@ export const i18nTexts = { forceMergeEnabledFieldLabel: i18n.translate('xpack.indexLifecycleMgmt.forcemerge.enableLabel', { defaultMessage: 'Force merge data', }), + maxNumSegmentsFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.forceMerge.numberOfSegmentsLabel', + { + defaultMessage: 'Number of segments', + } + ), + setPriorityFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.indexPriorityLabel', + { + defaultMessage: 'Index priority (optional)', + } + ), + bestCompressionFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.forcemerge.bestCompressionLabel', + { + defaultMessage: 'Compress stored fields', + } + ), + bestCompressionFieldHelpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.forceMerge.bestCompressionText', + { + defaultMessage: + 'Use higher compression for stored fields at the cost of slower performance.', + } + ), + allocationTypeOptionsFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.allocationFieldLabel', + { defaultMessage: 'Data tier options' } + ), + allocationNodeAttributeFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.dataTierAllocation.nodeAllocationFieldLabel', + { + defaultMessage: 'Select a node attribute', + } + ), + errors: { + numberRequired: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.numberRequiredErrorMessage', + { + defaultMessage: 'A number is required.', + } + ), + numberGreatThan0Required: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.numberAboveZeroRequiredError', + { + defaultMessage: 'Only numbers above 0 are allowed.', + } + ), + maximumAgeRequiredMessage: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.maximumAgeMissingError', + { + defaultMessage: 'A maximum age is required.', + } + ), + maximumSizeRequiredMessage: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.maximumIndexSizeMissingError', + { + defaultMessage: 'A maximum index size is required.', + } + ), + maximumDocumentsRequiredMessage: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.maximumDocumentsMissingError', + { + defaultMessage: 'Maximum documents is required.', + } + ), + rollOverConfigurationCallout: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.rolloverConfigurationError.title', + { + defaultMessage: 'Invalid rollover configuration', + } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.rolloverConfigurationError.body', + { + defaultMessage: + 'A value for one of maximum size, maximum documents, or maximum age is required.', + } + ), + }, + nonNegativeNumberRequired: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.nonNegativeNumberRequiredError', + { + defaultMessage: 'Only non-negative numbers are allowed.', + } + ), + }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts index e0e1ad44f1557..564b5a2c4e397 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts @@ -4,40 +4,166 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SerializedPolicy } from '../../../../common/types'; +import { isEmpty } from 'lodash'; -import { FormInternal } from './types'; +import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../common/types'; + +import { FormInternal, DataAllocationMetaFields } from './types'; +import { isNumber } from '../../services/policies/policy_serialization'; + +const serializeAllocateAction = ( + { dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields, + newActions: SerializedActionWithAllocation = {}, + originalActions: SerializedActionWithAllocation = {} +): SerializedActionWithAllocation => { + const { allocate, migrate, ...rest } = newActions; + // First copy over all non-allocate and migrate actions. + const actions: SerializedActionWithAllocation = { allocate, migrate, ...rest }; + + switch (dataTierAllocationType) { + case 'node_attrs': + if (allocationNodeAttribute) { + const [name, value] = allocationNodeAttribute.split(':'); + actions.allocate = { + // copy over any other allocate details like "number_of_replicas" + ...actions.allocate, + require: { + [name]: value, + }, + }; + } else { + // The form has been configured to use node attribute based allocation but no node attribute + // was selected. We fall back to what was originally selected in this case. This might be + // migrate.enabled: "false" + actions.migrate = originalActions.migrate; + } + + // copy over the original include and exclude values until we can set them in the form. + if (!isEmpty(originalActions?.allocate?.include)) { + actions.allocate = { + ...actions.allocate, + include: { ...originalActions?.allocate?.include }, + }; + } + + if (!isEmpty(originalActions?.allocate?.exclude)) { + actions.allocate = { + ...actions.allocate, + exclude: { ...originalActions?.allocate?.exclude }, + }; + } + break; + case 'none': + actions.migrate = { enabled: false }; + break; + default: + } + return actions; +}; export const createSerializer = (originalPolicy?: SerializedPolicy) => ( data: FormInternal ): SerializedPolicy => { - const { _meta, ...rest } = data; + const { _meta, ...policy } = data; - if (!rest.phases || !rest.phases.hot) { - rest.phases = { hot: { actions: {} } }; + if (!policy.phases || !policy.phases.hot) { + policy.phases = { hot: { actions: {} } }; } - if (rest.phases.hot) { - rest.phases.hot.min_age = originalPolicy?.phases.hot?.min_age ?? '0ms'; + /** + * HOT PHASE SERIALIZATION + */ + if (policy.phases.hot) { + policy.phases.hot.min_age = originalPolicy?.phases.hot?.min_age ?? '0ms'; } - if (rest.phases.hot?.actions) { - if (rest.phases.hot.actions?.rollover && _meta.hot.useRollover) { - if (rest.phases.hot.actions.rollover.max_age) { - rest.phases.hot.actions.rollover.max_age = `${rest.phases.hot.actions.rollover.max_age}${_meta.hot.maxAgeUnit}`; + if (policy.phases.hot?.actions) { + if (policy.phases.hot.actions?.rollover && _meta.hot.useRollover) { + if (policy.phases.hot.actions.rollover.max_age) { + policy.phases.hot.actions.rollover.max_age = `${policy.phases.hot.actions.rollover.max_age}${_meta.hot.maxAgeUnit}`; } - if (rest.phases.hot.actions.rollover.max_size) { - rest.phases.hot.actions.rollover.max_size = `${rest.phases.hot.actions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; + if (policy.phases.hot.actions.rollover.max_size) { + policy.phases.hot.actions.rollover.max_size = `${policy.phases.hot.actions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; } - if (_meta.hot.bestCompression && rest.phases.hot.actions?.forcemerge) { - rest.phases.hot.actions.forcemerge.index_codec = 'best_compression'; + if (_meta.hot.bestCompression && policy.phases.hot.actions?.forcemerge) { + policy.phases.hot.actions.forcemerge.index_codec = 'best_compression'; } } else { - delete rest.phases.hot.actions?.rollover; + delete policy.phases.hot.actions?.rollover; + } + } + + /** + * WARM PHASE SERIALIZATION + */ + if (policy.phases.warm) { + // If warm phase on rollover is enabled, delete min age field + // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time + // They are mutually exclusive + if (_meta.hot.useRollover && _meta.warm.warmPhaseOnRollover) { + delete policy.phases.warm.min_age; + } else if ( + (!_meta.hot.useRollover || !_meta.warm.warmPhaseOnRollover) && + policy.phases.warm.min_age + ) { + policy.phases.warm.min_age = `${policy.phases.warm.min_age}${_meta.warm.minAgeUnit}`; + } + + policy.phases.warm.actions = serializeAllocateAction( + _meta.warm, + policy.phases.warm.actions, + originalPolicy?.phases.warm?.actions + ); + + if ( + policy.phases.warm.actions.allocate && + !policy.phases.warm.actions.allocate.require && + !isNumber(policy.phases.warm.actions.allocate.number_of_replicas) && + isEmpty(policy.phases.warm.actions.allocate.include) && + isEmpty(policy.phases.warm.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete policy.phases.warm.actions.allocate; + } + + if (_meta.warm.bestCompression && policy.phases.warm.actions?.forcemerge) { + policy.phases.warm.actions.forcemerge.index_codec = 'best_compression'; + } + } + + /** + * COLD PHASE SERIALIZATION + */ + if (policy.phases.cold) { + if (policy.phases.cold.min_age) { + policy.phases.cold.min_age = `${policy.phases.cold.min_age}${_meta.cold.minAgeUnit}`; + } + + policy.phases.cold.actions = serializeAllocateAction( + _meta.cold, + policy.phases.cold.actions, + originalPolicy?.phases.cold?.actions + ); + + if ( + policy.phases.cold.actions.allocate && + !policy.phases.cold.actions.allocate.require && + !isNumber(policy.phases.cold.actions.allocate.number_of_replicas) && + isEmpty(policy.phases.cold.actions.allocate.include) && + isEmpty(policy.phases.cold.actions.allocate.exclude) + ) { + // remove allocate action if it does not define require or number of nodes + // and both include and exclude are empty objects (ES will fail to parse if we don't) + delete policy.phases.cold.actions.allocate; + } + + if (_meta.cold.freezeEnabled) { + policy.phases.cold.actions.freeze = {}; } } - return rest; + return policy; }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index dba56eb8ecbf3..1884f8dbc0619 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -6,6 +6,38 @@ import { SerializedPolicy } from '../../../../common/types'; +export type DataTierAllocationType = 'node_roles' | 'node_attrs' | 'none'; + +export interface DataAllocationMetaFields { + dataTierAllocationType: DataTierAllocationType; + allocationNodeAttribute?: string; +} + +export interface MinAgeField { + minAgeUnit?: string; +} + +export interface ForcemergeFields { + forceMergeEnabled: boolean; + bestCompression: boolean; +} + +interface HotPhaseMetaFields extends ForcemergeFields { + useRollover: boolean; + maxStorageSizeUnit?: string; + maxAgeUnit?: string; +} + +interface WarmPhaseMetaFields extends DataAllocationMetaFields, MinAgeField, ForcemergeFields { + enabled: boolean; + warmPhaseOnRollover: boolean; +} + +interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField { + enabled: boolean; + freezeEnabled: boolean; +} + /** * Describes the shape of data after deserialization. */ @@ -15,12 +47,8 @@ export interface FormInternal extends SerializedPolicy { * certain form fields which affects what is ultimately serialized. */ _meta: { - hot: { - useRollover: boolean; - forceMergeEnabled: boolean; - bestCompression: boolean; - maxStorageSizeUnit?: string; - maxAgeUnit?: string; - }; + hot: HotPhaseMetaFields; + warm: WarmPhaseMetaFields; + cold: ColdPhaseMetaFields; }; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx index 3481a2f0d4a2a..b655cb771e94d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx @@ -193,7 +193,7 @@ export const TableContent: React.FunctionComponent = ({ icon: 'list', onClick: () => { navigateToApp('management', { - path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`, true)}`, + path: `/data/index_management${getIndexListUri(`ilm.policy:"${policy.name}"`, true)}`, }); }, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts deleted file mode 100644 index 9f5f603fbc564..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash'; -import { AllocateAction, ColdPhase, SerializedColdPhase } from '../../../../common/types'; -import { serializedPhaseInitialization } from '../../constants'; -import { isNumber, splitSizeAndUnits } from './policy_serialization'; -import { - numberRequiredMessage, - PhaseValidationErrors, - positiveNumberRequiredMessage, -} from './policy_validation'; -import { determineDataTierAllocationType } from '../../lib'; -import { serializePhaseWithAllocation } from './shared'; - -export const coldPhaseInitialization: ColdPhase = { - phaseEnabled: false, - selectedMinimumAge: '0', - selectedMinimumAgeUnits: 'd', - selectedNodeAttrs: '', - selectedReplicaCount: '', - freezeEnabled: false, - phaseIndexPriority: '', - dataTierAllocationType: 'default', -}; - -export const coldPhaseFromES = (phaseSerialized?: SerializedColdPhase): ColdPhase => { - const phase = { ...coldPhaseInitialization }; - if (phaseSerialized === undefined || phaseSerialized === null) { - return phase; - } - - phase.phaseEnabled = true; - - if (phaseSerialized.actions.allocate) { - phase.dataTierAllocationType = determineDataTierAllocationType( - phaseSerialized.actions.allocate - ); - } - - if (phaseSerialized.min_age) { - const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); - phase.selectedMinimumAge = minAge; - phase.selectedMinimumAgeUnits = minAgeUnits; - } - - if (phaseSerialized.actions) { - const actions = phaseSerialized.actions; - if (actions.allocate) { - const allocate = actions.allocate; - if (allocate.require) { - Object.entries(allocate.require).forEach((entry) => { - phase.selectedNodeAttrs = entry.join(':'); - }); - if (allocate.number_of_replicas) { - phase.selectedReplicaCount = allocate.number_of_replicas.toString(); - } - } - } - - if (actions.freeze) { - phase.freezeEnabled = true; - } - - if (actions.set_priority) { - phase.phaseIndexPriority = actions.set_priority.priority - ? actions.set_priority.priority.toString() - : ''; - } - } - - return phase; -}; - -export const coldPhaseToES = ( - phase: ColdPhase, - originalPhase: SerializedColdPhase | undefined -): SerializedColdPhase => { - if (!originalPhase) { - originalPhase = { ...serializedPhaseInitialization }; - } - - const esPhase = { ...originalPhase }; - - if (isNumber(phase.selectedMinimumAge)) { - esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; - } - - esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions); - - if (isNumber(phase.selectedReplicaCount)) { - esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); - esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.number_of_replicas; - } - } - - if ( - esPhase.actions.allocate && - !esPhase.actions.allocate.require && - !isNumber(esPhase.actions.allocate.number_of_replicas) && - isEmpty(esPhase.actions.allocate.include) && - isEmpty(esPhase.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete esPhase.actions.allocate; - } - - if (phase.freezeEnabled) { - esPhase.actions.freeze = {}; - } else { - delete esPhase.actions.freeze; - } - - if (isNumber(phase.phaseIndexPriority)) { - esPhase.actions.set_priority = { - priority: parseInt(phase.phaseIndexPriority, 10), - }; - } else { - delete esPhase.actions.set_priority; - } - - return esPhase; -}; - -export const validateColdPhase = (phase: ColdPhase): PhaseValidationErrors => { - if (!phase.phaseEnabled) { - return {}; - } - - const phaseErrors = {} as PhaseValidationErrors; - - // index priority is optional, but if it's set, it needs to be a positive number - if (phase.phaseIndexPriority) { - if (!isNumber(phase.phaseIndexPriority)) { - phaseErrors.phaseIndexPriority = [numberRequiredMessage]; - } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { - phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; - } - } - - // min age needs to be a positive number - if (!isNumber(phase.selectedMinimumAge)) { - phaseErrors.selectedMinimumAge = [numberRequiredMessage]; - } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { - phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; - } - - return { ...phaseErrors }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts index 5c7f04986827b..19481b39a2c80 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts @@ -7,9 +7,7 @@ // eslint-disable-next-line no-restricted-imports import cloneDeep from 'lodash/cloneDeep'; import { deserializePolicy, legacySerializePolicy } from './policy_serialization'; -import { defaultNewColdPhase, defaultNewDeletePhase, defaultNewWarmPhase } from '../../constants'; -import { DataTierAllocationType } from '../../../../common/types'; -import { coldPhaseInitialization } from './cold_phase'; +import { defaultNewDeletePhase } from '../../constants'; describe('Policy serialization', () => { test('serialize a policy using "default" data allocation', () => { @@ -18,19 +16,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - warm: { - ...defaultNewWarmPhase, - dataTierAllocationType: 'default', - // These selected attrs should be ignored - selectedNodeAttrs: 'another:thing', - phaseEnabled: true, - }, - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'default', - selectedNodeAttrs: 'another:thing', - phaseEnabled: true, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -38,34 +23,12 @@ describe('Policy serialization', () => { name: 'test', phases: { hot: { actions: {} }, - warm: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, - cold: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, }, } ) ).toEqual({ name: 'test', - phases: { - warm: { - actions: { - set_priority: { - priority: 50, - }, - }, - }, - cold: { - actions: { - set_priority: { - priority: 0, - }, - }, - min_age: '0d', - }, - }, + phases: {}, }); }); @@ -75,18 +38,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - warm: { - ...defaultNewWarmPhase, - dataTierAllocationType: 'custom', - selectedNodeAttrs: 'another:thing', - phaseEnabled: true, - }, - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'custom', - selectedNodeAttrs: 'another:thing', - phaseEnabled: true, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -94,60 +45,12 @@ describe('Policy serialization', () => { name: 'test', phases: { hot: { actions: {} }, - warm: { - actions: { - allocate: { - include: { keep: 'this' }, - exclude: { keep: 'this' }, - require: { something: 'here' }, - }, - }, - }, - cold: { - actions: { - allocate: { - include: { keep: 'this' }, - exclude: { keep: 'this' }, - require: { something: 'here' }, - }, - }, - }, }, } ) ).toEqual({ name: 'test', - phases: { - warm: { - actions: { - allocate: { - include: { keep: 'this' }, - exclude: { keep: 'this' }, - require: { - another: 'thing', - }, - }, - set_priority: { - priority: 50, - }, - }, - }, - cold: { - actions: { - allocate: { - include: { keep: 'this' }, - exclude: { keep: 'this' }, - require: { - another: 'thing', - }, - }, - set_priority: { - priority: 0, - }, - }, - min_age: '0d', - }, - }, + phases: {}, }); }); @@ -157,18 +60,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - warm: { - ...defaultNewWarmPhase, - dataTierAllocationType: 'custom', - selectedNodeAttrs: '', - phaseEnabled: true, - }, - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'custom', - selectedNodeAttrs: '', - phaseEnabled: true, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -176,37 +67,13 @@ describe('Policy serialization', () => { name: 'test', phases: { hot: { actions: {} }, - warm: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, - cold: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, }, } ) ).toEqual({ // There should be no allocation action in any phases... name: 'test', - phases: { - warm: { - actions: { - allocate: { include: {}, exclude: {}, require: { something: 'here' } }, - set_priority: { - priority: 50, - }, - }, - }, - cold: { - actions: { - allocate: { include: {}, exclude: {}, require: { something: 'here' } }, - set_priority: { - priority: 0, - }, - }, - min_age: '0d', - }, - }, + phases: {}, }); }); @@ -216,18 +83,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - warm: { - ...defaultNewWarmPhase, - dataTierAllocationType: 'none', - selectedNodeAttrs: 'ignore:this', - phaseEnabled: true, - }, - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'none', - selectedNodeAttrs: 'ignore:this', - phaseEnabled: true, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -235,55 +90,20 @@ describe('Policy serialization', () => { name: 'test', phases: { hot: { actions: {} }, - warm: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, - cold: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, }, } ) ).toEqual({ // There should be no allocation action in any phases... name: 'test', - phases: { - warm: { - actions: { - migrate: { - enabled: false, - }, - set_priority: { - priority: 50, - }, - }, - }, - cold: { - actions: { - migrate: { - enabled: false, - }, - set_priority: { - priority: 0, - }, - }, - min_age: '0d', - }, - }, + phases: {}, }); }); test('serialization does not alter the original policy', () => { const originalPolicy = { name: 'test', - phases: { - warm: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, - cold: { - actions: { allocate: { include: {}, exclude: {}, require: { something: 'here' } } }, - }, - }, + phases: {}, }; const originalClone = cloneDeep(originalPolicy); @@ -291,27 +111,10 @@ describe('Policy serialization', () => { const deserializedPolicy = { name: 'test', phases: { - warm: { - ...defaultNewWarmPhase, - dataTierAllocationType: 'none' as DataTierAllocationType, - selectedNodeAttrs: 'ignore:this', - phaseEnabled: true, - }, - cold: { - ...defaultNewColdPhase, - dataTierAllocationType: 'none' as DataTierAllocationType, - selectedNodeAttrs: 'ignore:this', - phaseEnabled: true, - }, - delete: { ...defaultNewDeletePhase }, }, }; - legacySerializePolicy(deserializedPolicy, originalPolicy); - deserializedPolicy.phases.warm.dataTierAllocationType = 'custom'; - legacySerializePolicy(deserializedPolicy, originalPolicy); - deserializedPolicy.phases.warm.dataTierAllocationType = 'default'; legacySerializePolicy(deserializedPolicy, originalPolicy); expect(originalPolicy).toEqual(originalClone); }); @@ -322,16 +125,6 @@ describe('Policy serialization', () => { { name: 'test', phases: { - warm: { - ...defaultNewWarmPhase, - phaseEnabled: true, - forceMergeEnabled: true, - selectedForceMergeSegments: '1', - bestCompressionEnabled: true, - }, - cold: { - ...defaultNewColdPhase, - }, delete: { ...defaultNewDeletePhase }, }, }, @@ -344,19 +137,7 @@ describe('Policy serialization', () => { ) ).toEqual({ name: 'test', - phases: { - warm: { - actions: { - forcemerge: { - max_num_segments: 1, - index_codec: 'best_compression', - }, - set_priority: { - priority: 50, - }, - }, - }, - }, + phases: {}, }); }); @@ -384,34 +165,12 @@ describe('Policy serialization', () => { }, }, }, - warm: { - actions: { - forcemerge: { - max_num_segments: 1, - index_codec: 'best_compression', - }, - set_priority: { - priority: 50, - }, - }, - }, }, }, }) ).toEqual({ name: 'test', phases: { - warm: { - ...defaultNewWarmPhase, - warmPhaseOnRollover: false, - phaseEnabled: true, - forceMergeEnabled: true, - selectedForceMergeSegments: '1', - bestCompressionEnabled: true, - }, - cold: { - ...coldPhaseInitialization, - }, delete: { ...defaultNewDeletePhase }, }, }); @@ -423,47 +182,17 @@ describe('Policy serialization', () => { { name: 'test', phases: { - warm: { - ...defaultNewWarmPhase, - phaseEnabled: true, - forceMergeEnabled: true, - selectedForceMergeSegments: '1', - bestCompressionEnabled: false, - }, - cold: { - ...defaultNewColdPhase, - }, delete: { ...defaultNewDeletePhase }, }, }, { name: 'test', - phases: { - warm: { - actions: { - forcemerge: { - max_num_segments: 1, - index_codec: 'best_compression', - }, - }, - }, - }, + phases: {}, } ) ).toEqual({ name: 'test', - phases: { - warm: { - actions: { - forcemerge: { - max_num_segments: 1, - }, - set_priority: { - priority: 50, - }, - }, - }, - }, + phases: {}, }); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts index 0dce7efce4623..55e9d88dcd383 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -6,15 +6,8 @@ import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types'; -import { - defaultNewColdPhase, - defaultNewDeletePhase, - defaultNewWarmPhase, - serializedPhaseInitialization, -} from '../../constants'; +import { defaultNewDeletePhase, serializedPhaseInitialization } from '../../constants'; -import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; -import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; import { deletePhaseFromES, deletePhaseToES } from './delete_phase'; export const splitSizeAndUnits = (field: string): { size: string; units: string } => { @@ -48,8 +41,6 @@ export const initializeNewPolicy = (newPolicyName: string = ''): LegacyPolicy => return { name: newPolicyName, phases: { - warm: { ...defaultNewWarmPhase }, - cold: { ...defaultNewColdPhase }, delete: { ...defaultNewDeletePhase }, }, }; @@ -64,8 +55,6 @@ export const deserializePolicy = (policy: PolicyFromES): LegacyPolicy => { return { name, phases: { - warm: warmPhaseFromES(phases.warm), - cold: coldPhaseFromES(phases.cold), delete: deletePhaseFromES(phases.delete), }, }; @@ -82,13 +71,6 @@ export const legacySerializePolicy = ( name: policy.name, phases: {}, } as SerializedPolicy; - if (policy.phases.warm.phaseEnabled) { - serializedPolicy.phases.warm = warmPhaseToES(policy.phases.warm, originalEsPolicy.phases.warm); - } - - if (policy.phases.cold.phaseEnabled) { - serializedPolicy.phases.cold = coldPhaseToES(policy.phases.cold, originalEsPolicy.phases.cold); - } if (policy.phases.delete.phaseEnabled) { serializedPolicy.phases.delete = deletePhaseToES( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts index eeceb97c409f5..79c909c433f33 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -5,15 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - ColdPhase, - DeletePhase, - LegacyPolicy, - PolicyFromES, - WarmPhase, -} from '../../../../common/types'; -import { validateWarmPhase } from './warm_phase'; -import { validateColdPhase } from './cold_phase'; +import { DeletePhase, LegacyPolicy, PolicyFromES } from '../../../../common/types'; import { validateDeletePhase } from './delete_phase'; export const propertyof = (propertyName: keyof T & string) => propertyName; @@ -89,8 +81,6 @@ export type PhaseValidationErrors = { }; export interface ValidationErrors { - warm: PhaseValidationErrors; - cold: PhaseValidationErrors; delete: PhaseValidationErrors; policyName: string[]; } @@ -128,20 +118,12 @@ export const validatePolicy = ( } } - const warmPhaseErrors = validateWarmPhase(policy.phases.warm); - const coldPhaseErrors = validateColdPhase(policy.phases.cold); const deletePhaseErrors = validateDeletePhase(policy.phases.delete); - const isValid = - policyNameErrors.length === 0 && - Object.keys(warmPhaseErrors).length === 0 && - Object.keys(coldPhaseErrors).length === 0 && - Object.keys(deletePhaseErrors).length === 0; + const isValid = policyNameErrors.length === 0 && Object.keys(deletePhaseErrors).length === 0; return [ isValid, { policyName: [...policyNameErrors], - warm: warmPhaseErrors, - cold: coldPhaseErrors, delete: deletePhaseErrors, }, ]; @@ -156,12 +138,6 @@ export const findFirstError = (errors?: ValidationErrors): string | undefined => return propertyof('policyName'); } - if (Object.keys(errors.warm).length > 0) { - return `${propertyof('warm')}.${Object.keys(errors.warm)[0]}`; - } - if (Object.keys(errors.cold).length > 0) { - return `${propertyof('cold')}.${Object.keys(errors.cold)[0]}`; - } if (Object.keys(errors.delete).length > 0) { return `${propertyof('delete')}.${Object.keys(errors.delete)[0]}`; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts deleted file mode 100644 index c29ae3ab22831..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/shared/serialize_phase_with_allocation.ts +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import cloneDeep from 'lodash/cloneDeep'; - -import { - AllocateAction, - PhaseWithAllocationAction, - SerializedPhase, -} from '../../../../../common/types'; - -export const serializePhaseWithAllocation = ( - phase: PhaseWithAllocationAction, - originalPhaseActions: SerializedPhase['actions'] = {} -): SerializedPhase['actions'] => { - const esPhaseActions: SerializedPhase['actions'] = cloneDeep(originalPhaseActions); - - if (phase.dataTierAllocationType === 'custom' && phase.selectedNodeAttrs) { - const [name, value] = phase.selectedNodeAttrs.split(':'); - esPhaseActions.allocate = esPhaseActions.allocate || ({} as AllocateAction); - esPhaseActions.allocate.require = { - [name]: value, - }; - } else if (phase.dataTierAllocationType === 'none') { - esPhaseActions.migrate = { enabled: false }; - if (esPhaseActions.allocate) { - delete esPhaseActions.allocate; - } - } else if (phase.dataTierAllocationType === 'default') { - if (esPhaseActions.allocate) { - delete esPhaseActions.allocate.require; - } - delete esPhaseActions.migrate; - } - - return esPhaseActions; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts deleted file mode 100644 index 436e5a222f86d..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash'; -import { AllocateAction, WarmPhase, SerializedWarmPhase } from '../../../../common/types'; -import { serializedPhaseInitialization } from '../../constants'; -import { isNumber, splitSizeAndUnits } from './policy_serialization'; - -import { - numberRequiredMessage, - PhaseValidationErrors, - positiveNumberRequiredMessage, - positiveNumbersAboveZeroErrorMessage, -} from './policy_validation'; - -import { determineDataTierAllocationType } from '../../lib'; -import { serializePhaseWithAllocation } from './shared'; - -const warmPhaseInitialization: WarmPhase = { - phaseEnabled: false, - warmPhaseOnRollover: false, - selectedMinimumAge: '0', - selectedMinimumAgeUnits: 'd', - selectedNodeAttrs: '', - selectedReplicaCount: '', - shrinkEnabled: false, - selectedPrimaryShardCount: '', - forceMergeEnabled: false, - selectedForceMergeSegments: '', - bestCompressionEnabled: false, - phaseIndexPriority: '', - dataTierAllocationType: 'default', -}; - -export const warmPhaseFromES = (phaseSerialized?: SerializedWarmPhase): WarmPhase => { - const phase: WarmPhase = { ...warmPhaseInitialization }; - - if (phaseSerialized === undefined || phaseSerialized === null) { - return phase; - } - - phase.phaseEnabled = true; - - if (phaseSerialized.actions.allocate) { - phase.dataTierAllocationType = determineDataTierAllocationType( - phaseSerialized.actions.allocate - ); - } - - if (phaseSerialized.min_age) { - if (phaseSerialized.min_age === '0ms') { - phase.warmPhaseOnRollover = true; - } else { - const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age); - phase.selectedMinimumAge = minAge; - phase.selectedMinimumAgeUnits = minAgeUnits; - } - } - if (phaseSerialized.actions) { - const actions = phaseSerialized.actions; - if (actions.allocate) { - const allocate = actions.allocate; - if (allocate.require) { - Object.entries(allocate.require).forEach((entry) => { - phase.selectedNodeAttrs = entry.join(':'); - }); - if (allocate.number_of_replicas) { - phase.selectedReplicaCount = allocate.number_of_replicas.toString(); - } - } - } - - if (actions.forcemerge) { - const forcemerge = actions.forcemerge; - phase.forceMergeEnabled = true; - phase.selectedForceMergeSegments = forcemerge.max_num_segments.toString(); - // only accepted value for index_codec - phase.bestCompressionEnabled = forcemerge.index_codec === 'best_compression'; - } - - if (actions.shrink) { - phase.shrinkEnabled = true; - phase.selectedPrimaryShardCount = actions.shrink.number_of_shards - ? actions.shrink.number_of_shards.toString() - : ''; - } - - if (actions.set_priority) { - phase.phaseIndexPriority = actions.set_priority.priority - ? actions.set_priority.priority.toString() - : ''; - } - } - return phase; -}; - -export const warmPhaseToES = ( - phase: WarmPhase, - originalEsPhase?: SerializedWarmPhase -): SerializedWarmPhase => { - if (!originalEsPhase) { - originalEsPhase = { ...serializedPhaseInitialization }; - } - - const esPhase = { ...originalEsPhase }; - - if (isNumber(phase.selectedMinimumAge)) { - esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`; - } - - // If warm phase on rollover is enabled, delete min age field - // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time - // They are mutually exclusive - if (phase.warmPhaseOnRollover) { - delete esPhase.min_age; - } - - esPhase.actions = serializePhaseWithAllocation(phase, esPhase.actions); - - if (isNumber(phase.selectedReplicaCount)) { - esPhase.actions.allocate = esPhase.actions.allocate || ({} as AllocateAction); - esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); - } else { - if (esPhase.actions.allocate) { - delete esPhase.actions.allocate.number_of_replicas; - } - } - - if ( - esPhase.actions.allocate && - !esPhase.actions.allocate.require && - !isNumber(esPhase.actions.allocate.number_of_replicas) && - isEmpty(esPhase.actions.allocate.include) && - isEmpty(esPhase.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete esPhase.actions.allocate; - } - - if (phase.forceMergeEnabled) { - esPhase.actions.forcemerge = { - max_num_segments: parseInt(phase.selectedForceMergeSegments, 10), - }; - if (phase.bestCompressionEnabled) { - // only accepted value for index_codec - esPhase.actions.forcemerge.index_codec = 'best_compression'; - } - } else { - delete esPhase.actions.forcemerge; - } - - if (phase.shrinkEnabled && isNumber(phase.selectedPrimaryShardCount)) { - esPhase.actions.shrink = { - number_of_shards: parseInt(phase.selectedPrimaryShardCount, 10), - }; - } else { - delete esPhase.actions.shrink; - } - - if (isNumber(phase.phaseIndexPriority)) { - esPhase.actions.set_priority = { - priority: parseInt(phase.phaseIndexPriority, 10), - }; - } else { - delete esPhase.actions.set_priority; - } - - return esPhase; -}; - -export const validateWarmPhase = (phase: WarmPhase): PhaseValidationErrors => { - if (!phase.phaseEnabled) { - return {}; - } - - const phaseErrors = {} as PhaseValidationErrors; - - // index priority is optional, but if it's set, it needs to be a positive number - if (phase.phaseIndexPriority) { - if (!isNumber(phase.phaseIndexPriority)) { - phaseErrors.phaseIndexPriority = [numberRequiredMessage]; - } else if (parseInt(phase.phaseIndexPriority, 10) < 0) { - phaseErrors.phaseIndexPriority = [positiveNumberRequiredMessage]; - } - } - - // if warm phase on rollover is disabled, min age needs to be a positive number - if (!phase.warmPhaseOnRollover) { - if (!isNumber(phase.selectedMinimumAge)) { - phaseErrors.selectedMinimumAge = [numberRequiredMessage]; - } else if (parseInt(phase.selectedMinimumAge, 10) < 0) { - phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage]; - } - } - - // if forcemerge is enabled, force merge segments needs to be a number above zero - if (phase.forceMergeEnabled) { - if (!isNumber(phase.selectedForceMergeSegments)) { - phaseErrors.selectedForceMergeSegments = [numberRequiredMessage]; - } else if (parseInt(phase.selectedForceMergeSegments, 10) < 1) { - phaseErrors.selectedForceMergeSegments = [positiveNumbersAboveZeroErrorMessage]; - } - } - - // if shrink is enabled, primary shard count needs to be a number above zero - if (phase.shrinkEnabled) { - if (!isNumber(phase.selectedPrimaryShardCount)) { - phaseErrors.selectedPrimaryShardCount = [numberRequiredMessage]; - } else if (parseInt(phase.selectedPrimaryShardCount, 10) < 1) { - phaseErrors.selectedPrimaryShardCount = [positiveNumbersAboveZeroErrorMessage]; - } - } - - // replica count is optional, but if it's set, it needs to be a positive number - if (phase.selectedReplicaCount) { - if (!isNumber(phase.selectedReplicaCount)) { - phaseErrors.selectedReplicaCount = [numberRequiredMessage]; - } else if (parseInt(phase.selectedReplicaCount, 10) < 0) { - phaseErrors.selectedReplicaCount = [numberRequiredMessage]; - } - } - - return { - ...phaseErrors, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts index 81eb1c8cad135..2f1c7798e7a4d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts @@ -9,8 +9,7 @@ import { UIM_CONFIG_WARM_PHASE, UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_FREEZE_INDEX, - defaultNewWarmPhase, - defaultNewColdPhase, + defaultPhaseIndexPriority, } from '../constants/'; import { getUiMetricsForPhases } from './ui_metric'; @@ -23,7 +22,7 @@ describe('getUiMetricsForPhases', () => { min_age: '0ms', actions: { set_priority: { - priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), + priority: parseInt(defaultPhaseIndexPriority, 10), }, }, }, @@ -38,7 +37,7 @@ describe('getUiMetricsForPhases', () => { min_age: '0ms', actions: { set_priority: { - priority: parseInt(defaultNewWarmPhase.phaseIndexPriority, 10), + priority: parseInt(defaultPhaseIndexPriority, 10), }, }, }, @@ -53,7 +52,7 @@ describe('getUiMetricsForPhases', () => { min_age: '0ms', actions: { set_priority: { - priority: parseInt(defaultNewWarmPhase.phaseIndexPriority, 10) + 1, + priority: parseInt(defaultPhaseIndexPriority, 10) + 1, }, }, }, @@ -69,7 +68,7 @@ describe('getUiMetricsForPhases', () => { actions: { freeze: {}, set_priority: { - priority: parseInt(defaultNewColdPhase.phaseIndexPriority, 10), + priority: parseInt(defaultPhaseIndexPriority, 10), }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index ea5c5619da589..274d3d1ca97f3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -13,9 +13,8 @@ import { UIM_CONFIG_FREEZE_INDEX, UIM_CONFIG_SET_PRIORITY, UIM_CONFIG_WARM_PHASE, - defaultNewColdPhase, - defaultNewWarmPhase, defaultSetPriority, + defaultPhaseIndexPriority, } from '../constants'; import { Phases } from '../../../common/types'; @@ -50,14 +49,12 @@ export function getUiMetricsForPhases(phases: Phases): string[] { const isWarmPhasePriorityChanged = phases.warm && phases.warm.actions.set_priority && - phases.warm.actions.set_priority.priority !== - parseInt(defaultNewWarmPhase.phaseIndexPriority, 10); + phases.warm.actions.set_priority.priority !== parseInt(defaultPhaseIndexPriority, 10); const isColdPhasePriorityChanged = phases.cold && phases.cold.actions.set_priority && - phases.cold.actions.set_priority.priority !== - parseInt(defaultNewColdPhase.phaseIndexPriority, 10); + phases.cold.actions.set_priority.priority !== parseInt(defaultPhaseIndexPriority, 10); // If the priority is different than the default, we'll consider it a user interaction, // even if the user has set it to undefined. return ( diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts index dc3e1b1d1b62d..023aeba57aa7a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -26,6 +26,7 @@ export { ToggleField, NumericField, SelectField, + SuperSelectField, } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts index 2fcf2a822cb2d..82bd858240e1e 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -186,3 +186,28 @@ export const createDataStreamPayload = (name: string): DataStream => ({ storageSize: '1b', maxTimeStamp: 420, }); + +export const createDataStreamBackingIndex = (indexName: string, dataStreamName: string) => ({ + health: '', + status: '', + primary: '', + replica: '', + documents: '', + documents_deleted: '', + size: '', + primary_size: '', + name: indexName, + data_stream: dataStreamName, +}); + +export const createNonDataStreamIndex = (name: string) => ({ + health: 'green', + status: 'open', + primary: 1, + replica: 1, + documents: 10000, + documents_deleted: 100, + size: '156kb', + primary_size: '156kb', + name, +}); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index ade4a62ceb198..23539659d2af2 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -9,7 +9,13 @@ import { act } from 'react-dom/test-utils'; import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment } from '../helpers'; -import { DataStreamsTabTestBed, setup, createDataStreamPayload } from './data_streams_tab.helpers'; +import { + DataStreamsTabTestBed, + setup, + createDataStreamPayload, + createDataStreamBackingIndex, + createNonDataStreamIndex, +} from './data_streams_tab.helpers'; describe('Data Streams tab', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -85,29 +91,8 @@ describe('Data Streams tab', () => { } = httpRequestsMockHelpers; setLoadIndicesResponse([ - { - health: '', - status: '', - primary: '', - replica: '', - documents: '', - documents_deleted: '', - size: '', - primary_size: '', - name: 'data-stream-index', - data_stream: 'dataStream1', - }, - { - health: 'green', - status: 'open', - primary: 1, - replica: 1, - documents: 10000, - documents_deleted: 100, - size: '156kb', - primary_size: '156kb', - name: 'non-data-stream-index', - }, + createDataStreamBackingIndex('data-stream-index', 'dataStream1'), + createNonDataStreamIndex('non-data-stream-index'), ]); const dataStreamForDetailPanel = createDataStreamPayload('dataStream1'); @@ -260,4 +245,46 @@ describe('Data Streams tab', () => { }); }); }); + + describe('when there are special characters', () => { + beforeEach(async () => { + const { + setLoadIndicesResponse, + setLoadDataStreamsResponse, + setLoadDataStreamResponse, + } = httpRequestsMockHelpers; + + setLoadIndicesResponse([ + createDataStreamBackingIndex('data-stream-index', '%dataStream'), + createDataStreamBackingIndex('data-stream-index2', 'dataStream2'), + ]); + + const dataStreamDollarSign = createDataStreamPayload('%dataStream'); + setLoadDataStreamsResponse([dataStreamDollarSign]); + setLoadDataStreamResponse(dataStreamDollarSign); + + testBed = await setup(); + await act(async () => { + testBed.actions.goToDataStreamsList(); + }); + testBed.component.update(); + }); + + describe('detail panel', () => { + test('opens when the data stream name in the table is clicked', async () => { + const { actions, findDetailPanel, findDetailPanelTitle } = testBed; + await actions.clickNameAt(0); + expect(findDetailPanel().length).toBe(1); + expect(findDetailPanelTitle()).toBe('%dataStream'); + }); + + test('clicking the indices count navigates to the backing indices', async () => { + const { table, actions } = testBed; + await actions.clickIndicesAt(0); + expect(table.getMetaData('indexTable').tableCellsValues).toEqual([ + ['', '', '', '', '', '', '', '%dataStream'], + ]); + }); + }); + }); }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx index 0f5bc64c358b9..6d9aa58d6c86b 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx @@ -20,12 +20,17 @@ import { EuiBadge, } from '@elastic/eui'; -import { SectionLoading, TabSettings, TabAliases, TabMappings } from '../shared_imports'; +import { + SectionLoading, + TabSettings, + TabAliases, + TabMappings, + attemptToURIDecode, +} from '../shared_imports'; import { useComponentTemplatesContext } from '../component_templates_context'; import { TabSummary } from './tab_summary'; import { ComponentTemplateTabs, TabType } from './tabs'; import { ManageButton, ManageAction } from './manage_button'; -import { attemptToDecodeURI } from '../lib'; export interface Props { componentTemplateName: string; @@ -47,7 +52,7 @@ export const ComponentTemplateDetailsFlyoutContent: React.FunctionComponent { const { api } = useComponentTemplatesContext(); - const decodedComponentTemplateName = attemptToDecodeURI(componentTemplateName); + const decodedComponentTemplateName = attemptToURIDecode(componentTemplateName); const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate( decodedComponentTemplateName diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 05f7f53969ded..00ea3ebf794ee 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -11,9 +11,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ScopedHistory } from 'kibana/public'; import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; +import { attemptToURIDecode } from '../../../../shared_imports'; import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants'; -import { attemptToDecodeURI } from '../lib'; import { useComponentTemplatesContext } from '../component_templates_context'; import { ComponentTemplateDetailsFlyoutContent, @@ -84,7 +84,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ }), icon: 'pencil', handleActionClick: () => - goToEditComponentTemplate(attemptToDecodeURI(componentTemplateName)), + goToEditComponentTemplate(attemptToURIDecode(componentTemplateName)), }, { name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', { @@ -92,7 +92,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ }), icon: 'copy', handleActionClick: () => - goToCloneComponentTemplate(attemptToDecodeURI(componentTemplateName)), + goToCloneComponentTemplate(attemptToURIDecode(componentTemplateName)), }, { name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', { @@ -103,7 +103,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ details._kbnMeta.usedBy.length > 0, closePopoverOnClick: true, handleActionClick: () => { - setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]); + setComponentTemplatesToDelete([attemptToURIDecode(componentTemplateName)]); }, }, ]; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx index 94db623f313c7..6c03fcf5d9972 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx @@ -9,9 +9,8 @@ import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SectionLoading } from '../../shared_imports'; +import { SectionLoading, attemptToURIDecode } from '../../shared_imports'; import { useComponentTemplatesContext } from '../../component_templates_context'; -import { attemptToDecodeURI } from '../../lib'; import { ComponentTemplateCreate } from '../component_template_create'; export interface Params { @@ -20,7 +19,7 @@ export interface Params { export const ComponentTemplateClone: FunctionComponent> = (props) => { const { sourceComponentTemplateName } = props.match.params; - const decodedSourceName = attemptToDecodeURI(sourceComponentTemplateName); + const decodedSourceName = attemptToURIDecode(sourceComponentTemplateName); const { toasts, api } = useComponentTemplatesContext(); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx index 2bd3dfb34acb9..934f86f7d7590 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -9,8 +9,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { useComponentTemplatesContext } from '../../component_templates_context'; -import { ComponentTemplateDeserialized, SectionLoading } from '../../shared_imports'; -import { attemptToDecodeURI } from '../../lib'; +import { + ComponentTemplateDeserialized, + SectionLoading, + attemptToURIDecode, +} from '../../shared_imports'; import { ComponentTemplateForm } from '../component_template_form'; interface MatchParams { @@ -28,7 +31,7 @@ export const ComponentTemplateEdit: React.FunctionComponent(false); const [saveError, setSaveError] = useState(null); - const decodedName = attemptToDecodeURI(name); + const decodedName = attemptToURIDecode(name); const { error, data: componentTemplate, isLoading } = api.useLoadComponentTemplate(decodedName); @@ -50,7 +53,9 @@ export const ComponentTemplateEdit: React.FunctionComponent { - let result: string; - - try { - result = decodeURI(value); - result = decodeURIComponent(result); - } catch (e) { - result = decodeURIComponent(value); - } - - return result; -}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index ffd78925c16a0..4a9771bd5ba8a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -20,6 +20,7 @@ export { NotAuthorizedSection, Forms, GlobalFlyout, + attemptToURIDecode, } from '../../../../../../../src/plugins/es_ui_shared/public'; export { diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index 4f2a5c4a27b7a..19286523055f5 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -20,13 +20,17 @@ import { } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; -import { reactRouterNavigate, extractQueryParams } from '../../../../shared_imports'; +import { + reactRouterNavigate, + extractQueryParams, + attemptToURIDecode, +} from '../../../../shared_imports'; import { useAppContext } from '../../../app_context'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadDataStreams } from '../../../services/api'; -import { encodePathForReactRouter, decodePathFromReactRouter } from '../../../services/routing'; +import { getIndexListUri } from '../../../services/routing'; import { documentationService } from '../../../services/documentation'; -import { Section } from '../../home'; +import { Section } from '../home'; import { DataStreamTable } from './data_stream_table'; import { DataStreamDetailPanel } from './data_stream_detail_panel'; @@ -206,7 +210,7 @@ export const DataStreamList: React.FunctionComponent { history.push(`/${Section.DataStreams}`); diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx index 642123c511419..992d569bf3a42 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx @@ -12,9 +12,8 @@ import { ScopedHistory } from 'kibana/public'; import { DataStream } from '../../../../../../common/types'; import { UseRequestResponse, reactRouterNavigate } from '../../../../../shared_imports'; -import { encodePathForReactRouter } from '../../../../services/routing'; +import { getDataStreamDetailsLink, getIndexListUri } from '../../../../services/routing'; import { DataHealth } from '../../../../components'; -import { Section } from '../../../home'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; @@ -45,13 +44,11 @@ export const DataStreamTable: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - render: (name: DataStream['name'], item: DataStream) => { + render: (name: DataStream['name']) => { return ( {name} @@ -108,12 +105,7 @@ export const DataStreamTable: React.FunctionComponent = ({ render: (indices: DataStream['indices'], dataStream) => ( {indices.length} diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index b4e003b667074..7b09f20091110 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -35,9 +35,9 @@ import { } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; -import { reactRouterNavigate } from '../../../../../shared_imports'; +import { reactRouterNavigate, attemptToURIDecode } from '../../../../../shared_imports'; import { REFRESH_RATE_INDEX_LIST } from '../../../../constants'; -import { encodePathForReactRouter } from '../../../../services/routing'; +import { getDataStreamDetailsLink } from '../../../../services/routing'; import { documentationService } from '../../../../services/documentation'; import { AppContextConsumer } from '../../../../app_context'; import { renderBadges } from '../../../../lib/render_badges'; @@ -107,7 +107,7 @@ export class IndexTable extends Component { const { location, filterChanged } = this.props; const { filter } = qs.parse((location && location.search) || ''); if (filter) { - const decodedFilter = decodeURIComponent(filter); + const decodedFilter = attemptToURIDecode(filter); try { const filter = EuiSearchBar.Query.parse(decodedFilter); @@ -279,7 +279,7 @@ export class IndexTable extends Component { diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx index 7ec6f1f94a2ab..29b841f3bdc7a 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -13,7 +13,7 @@ import { UseRequestResponse, reactRouterNavigate } from '../../../../../../share import { TemplateListItem } from '../../../../../../../common'; import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/constants'; import { TemplateDeleteModal } from '../../../../../components'; -import { encodePathForReactRouter } from '../../../../../services/routing'; +import { getTemplateDetailsLink } from '../../../../../services/routing'; import { useServices } from '../../../../../app_context'; import { TemplateContentIndicator } from '../../../../../components/shared'; import { TemplateTypeIndicator } from '../../components'; @@ -53,10 +53,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) )} data-test-subj="templateDetailsLink" diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx index 48083f324de3d..4899c5c664eba 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -34,7 +34,6 @@ import { import { UseRequestResponse } from '../../../../../shared_imports'; import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; import { useLoadIndexTemplate } from '../../../../services/api'; -import { decodePathFromReactRouter } from '../../../../services/routing'; import { useServices } from '../../../../app_context'; import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared'; import { TemplateTypeIndicator } from '../components'; @@ -103,11 +102,7 @@ export const TemplateDetailsContent = ({ reload, }: Props) => { const { uiMetricService } = useServices(); - const decodedTemplateName = decodePathFromReactRouter(templateName); - const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( - decodedTemplateName, - isLegacy - ); + const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(templateName, isLegacy); const isCloudManaged = templateDetails?._kbnMeta.type === 'cloudManaged'; const [templateToDelete, setTemplateToDelete] = useState< Array<{ name: string; isLegacy?: boolean }> @@ -120,7 +115,7 @@ export const TemplateDetailsContent = ({

- {decodedTemplateName} + {templateName} {templateDetails && ( <>   @@ -303,8 +298,7 @@ export const TemplateDetailsContent = ({ defaultMessage: 'Delete', }), icon: 'trash', - onClick: () => - setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]), + onClick: () => setTemplateToDelete([{ name: templateName, isLegacy }]), disabled: isCloudManaged, }, ], diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index c711f457123fb..f3e82223c30e6 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -36,6 +36,7 @@ import { TemplateTable } from './template_table'; import { TemplateDetails } from './template_details'; import { LegacyTemplateTable } from './legacy_templates/template_table'; import { FilterListButton, Filters } from './components'; +import { attemptToURIDecode } from '../../../../shared_imports'; type FilterName = 'managed' | 'cloudManaged' | 'system'; interface MatchParams { @@ -100,7 +101,7 @@ export const TemplateList: React.FunctionComponent = ({ return ( <> uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) + {...reactRouterNavigate(history, getTemplateDetailsLink(name), () => + uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) )} data-test-subj="templateDetailsLink" > diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index 2aaecbd64ee28..5bb355d90478b 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -11,9 +11,10 @@ import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../common'; import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; -import { decodePathFromReactRouter, getTemplateDetailsLink } from '../../services/routing'; +import { getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; +import { attemptToURIDecode } from '../../../shared_imports'; interface MatchParams { name: string; @@ -26,7 +27,7 @@ export const TemplateClone: React.FunctionComponent { - const decodedTemplateName = decodePathFromReactRouter(name); + const decodedTemplateName = attemptToURIDecode(name); const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index 6bdcd03fa5ca4..3e62f7f880f74 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -11,9 +11,10 @@ import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@e import { TemplateDeserialized } from '../../../../common'; import { breadcrumbService } from '../../services/breadcrumbs'; import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; -import { decodePathFromReactRouter, getTemplateDetailsLink } from '../../services/routing'; +import { getTemplateDetailsLink } from '../../services/routing'; import { SectionLoading, SectionError, TemplateForm, Error } from '../../components'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; +import { attemptToURIDecode } from '../../../shared_imports'; interface MatchParams { name: string; @@ -26,7 +27,7 @@ export const TemplateEdit: React.FunctionComponent { - const decodedTemplateName = decodePathFromReactRouter(name); + const decodedTemplateName = attemptToURIDecode(name); const isLegacy = getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); @@ -51,7 +52,7 @@ export const TemplateEdit: React.FunctionComponent { diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts index 68bf06409e6ab..d12198d6b17a5 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -6,9 +6,8 @@ export const getTemplateListLink = () => `/templates`; -export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHash = false) => { - const baseUrl = `/templates/${encodePathForReactRouter(name)}`; - let url = withHash ? `#${baseUrl}` : baseUrl; +export const getTemplateDetailsLink = (name: string, isLegacy?: boolean) => { + let url = `/templates/${encodeURIComponent(name)}`; if (isLegacy) { url = `${url}?legacy=${isLegacy}`; } @@ -16,7 +15,7 @@ export const getTemplateDetailsLink = (name: string, isLegacy?: boolean, withHas }; export const getTemplateEditLink = (name: string, isLegacy?: boolean) => { - let url = `/edit_template/${encodePathForReactRouter(name)}`; + let url = `/edit_template/${encodeURIComponent(name)}`; if (isLegacy) { url = `${url}?legacy=true`; } @@ -24,7 +23,7 @@ export const getTemplateEditLink = (name: string, isLegacy?: boolean) => { }; export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => { - let url = `/clone_template/${encodePathForReactRouter(name)}`; + let url = `/clone_template/${encodeURIComponent(name)}`; if (isLegacy) { url = `${url}?legacy=true`; } @@ -32,9 +31,7 @@ export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => { }; export const getILMPolicyPath = (policyName: string) => { - return encodeURI( - `/data/index_lifecycle_management/policies/edit/${encodeURIComponent(policyName)}` - ); + return `/data/index_lifecycle_management/policies/edit/${encodeURIComponent(policyName)}`; }; export const getIndexListUri = (filter?: string, includeHiddenIndices?: boolean) => { @@ -53,18 +50,6 @@ export const getIndexListUri = (filter?: string, includeHiddenIndices?: boolean) return '/indices'; }; -export const decodePathFromReactRouter = (pathname: string): string => { - let decodedPath; - try { - decodedPath = decodeURI(pathname); - decodedPath = decodeURIComponent(decodedPath); - } catch (_error) { - decodedPath = decodeURIComponent(pathname); - } - return decodeURIComponent(decodedPath); +export const getDataStreamDetailsLink = (name: string) => { + return encodeURI(`/data_streams/${encodeURIComponent(name)}`); }; - -// Need to add some additonal encoding/decoding logic to work with React Router -// For background, see: https://github.com/ReactTraining/history/issues/505 -export const encodePathForReactRouter = (pathname: string): string => - encodeURIComponent(encodeURIComponent(pathname)); diff --git a/x-pack/plugins/index_management/public/services/extensions_service.mock.ts b/x-pack/plugins/index_management/public/services/extensions_service.mock.ts index 3075a114eedd1..6cf533d67662f 100644 --- a/x-pack/plugins/index_management/public/services/extensions_service.mock.ts +++ b/x-pack/plugins/index_management/public/services/extensions_service.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ExtensionsService, ExtensionsSetup } from './extensions_service'; export type ExtensionsSetupMock = jest.Mocked; diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index acb3eb512e2c1..360d3ba473db8 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -14,6 +14,7 @@ export { Forms, extractQueryParams, GlobalFlyout, + attemptToURIDecode, } from '../../../../src/plugins/es_ui_shared/public'; export { diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts index 34edcb6fb7477..fa93d9bd0c563 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts @@ -83,15 +83,16 @@ export function registerGetOneRoute({ router, license, lib: { isEsError } }: Rou license.guardApiRoute(async (ctx, req, res) => { const { name } = req.params as TypeOf; const { callAsCurrentUser } = ctx.dataManagement!.client; - try { const [ { data_streams: dataStream }, { data_streams: dataStreamsStats }, ] = await Promise.all([ - callAsCurrentUser('dataManagement.getDataStream', { name }), + callAsCurrentUser('dataManagement.getDataStream', { + name, + }), ctx.core.elasticsearch.legacy.client.callAsCurrentUser('transport.request', { - path: `/_data_stream/${name}/_stats`, + path: `/_data_stream/${encodeURIComponent(name)}/_stats`, method: 'GET', query: { human: true, diff --git a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts index 5014aeb52a3f7..d38b4690fed71 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts @@ -5,6 +5,7 @@ */ import * as rt from 'io-ts'; +import { jsonArrayRT } from '../../typed_json'; import { logEntriesCursorRT } from './common'; export const LOG_ENTRIES_PATH = '/api/log_entries/entries'; @@ -54,7 +55,7 @@ export const logMessageConstantPartRT = rt.type({ }); export const logMessageFieldPartRT = rt.type({ field: rt.string, - value: rt.unknown, + value: jsonArrayRT, highlights: rt.array(rt.string), }); @@ -64,7 +65,7 @@ export const logTimestampColumnRT = rt.type({ columnId: rt.string, timestamp: rt export const logFieldColumnRT = rt.type({ columnId: rt.string, field: rt.string, - value: rt.unknown, + value: jsonArrayRT, highlights: rt.array(rt.string), }); export const logMessageColumnRT = rt.type({ diff --git a/x-pack/plugins/infra/common/http_api/log_entries/item.ts b/x-pack/plugins/infra/common/http_api/log_entries/item.ts index 02335d68402c0..5f9457b8228ac 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/item.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/item.ts @@ -16,7 +16,7 @@ export const logEntriesItemRequestRT = rt.type({ export type LogEntriesItemRequest = rt.TypeOf; -const logEntriesItemFieldRT = rt.type({ field: rt.string, value: rt.string }); +const logEntriesItemFieldRT = rt.type({ field: rt.string, value: rt.array(rt.string) }); const logEntriesItemRT = rt.type({ id: rt.string, index: rt.string, diff --git a/x-pack/plugins/infra/common/http_api/metrics_api.ts b/x-pack/plugins/infra/common/http_api/metrics_api.ts index 41657fdce2153..14b7c24e57694 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_api.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts @@ -30,6 +30,7 @@ export const MetricsAPIRequestRT = rt.intersection([ }), rt.partial({ groupBy: rt.array(groupByRT), + modules: rt.array(rt.string), afterKey: rt.union([rt.null, afterKeyObjectRT]), limit: rt.union([rt.number, rt.null, rt.undefined]), filters: rt.array(rt.object), diff --git a/x-pack/plugins/infra/common/typed_json.ts b/x-pack/plugins/infra/common/typed_json.ts index 98b5456fe44b8..0ff9e42942ef2 100644 --- a/x-pack/plugins/infra/common/typed_json.ts +++ b/x-pack/plugins/infra/common/typed_json.ts @@ -4,11 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; +import * as rt from 'io-ts'; +import { JsonArray, JsonObject, JsonValue } from '../../../../src/plugins/kibana_utils/common'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface JsonArray extends Array {} +export const jsonScalarRT = rt.union([rt.null, rt.boolean, rt.number, rt.string]); -export interface JsonObject { - [key: string]: JsonValue; -} +export const jsonValueRT: rt.Type = rt.recursion('JsonValue', () => + rt.union([jsonScalarRT, jsonArrayRT, jsonObjectRT]) +); + +export const jsonArrayRT: rt.Type = rt.recursion('JsonArray', () => + rt.array(jsonValueRT) +); + +export const jsonObjectRT: rt.Type = rt.recursion('JsonObject', () => + rt.record(rt.string, jsonValueRT) +); + +export { JsonValue, JsonArray, JsonObject }; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx index b4fa6b8800fba..77154474077c8 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx @@ -29,7 +29,7 @@ describe('LogEntryActionsMenu component', () => { { { { { { { .filter(({ field, value }) => value != null && UPTIME_FIELDS.includes(field)) .reduce((acc, fieldItem) => { const { field, value } = fieldItem; - try { - const parsedValue = decodeOrThrow(rt.array(rt.string))(JSON.parse(value)); - return acc.concat(parsedValue.map((val) => `${field}:${val}`)); - } catch (e) { - return acc.concat([`${field}:${value}`]); - } + return acc.concat(value.map((val) => `${field}:${val}`)); }, []); if (searchExpressions.length === 0) { @@ -119,7 +112,7 @@ const getUptimeLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { const getAPMLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { const traceIdEntry = logItem.fields.find( - ({ field, value }) => value != null && field === 'trace.id' + ({ field, value }) => value[0] != null && field === 'trace.id' ); if (!traceIdEntry) { @@ -127,7 +120,7 @@ const getAPMLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { } const timestampField = logItem.fields.find(({ field }) => field === '@timestamp'); - const timestamp = timestampField ? timestampField.value : null; + const timestamp = timestampField ? timestampField.value[0] : null; const { rangeFrom, rangeTo } = timestamp ? (() => { const from = new Date(timestamp); @@ -142,6 +135,6 @@ const getAPMLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { return { app: 'apm', - hash: getTraceUrl({ traceId: traceIdEntry.value, rangeFrom, rangeTo }), + hash: getTraceUrl({ traceId: traceIdEntry.value[0], rangeFrom, rangeTo }), }; }; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx index 76ffada510e51..b07d8c9dce23c 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx @@ -94,7 +94,7 @@ export const LogEntryFlyout = ({ onClick={createFilterHandler(item)} /> - {item.value} + {formatValue(item.value)} ), }, @@ -147,3 +147,7 @@ export const InfraFlyoutLoadingPanel = euiStyled.div` bottom: 0; left: 0; `; + +function formatValue(value: string[]) { + return value.length > 1 ? value.join(', ') : value[0]; +} diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx new file mode 100644 index 0000000000000..7caf34c0cd6b7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import stringify from 'json-stable-stringify'; +import React from 'react'; +import { euiStyled } from '../../../../../observability/public'; +import { JsonArray, JsonValue } from '../../../../common/typed_json'; +import { ActiveHighlightMarker, highlightFieldValue, HighlightMarker } from './highlighting'; + +export const FieldValue: React.FC<{ + highlightTerms: string[]; + isActiveHighlight: boolean; + value: JsonArray; +}> = React.memo(({ highlightTerms, isActiveHighlight, value }) => { + if (value.length === 1) { + return ( + <> + {highlightFieldValue( + formatValue(value[0]), + highlightTerms, + isActiveHighlight ? ActiveHighlightMarker : HighlightMarker + )} + + ); + } else if (value.length > 1) { + return ( +
    + {value.map((entry, i) => ( + + {highlightFieldValue( + formatValue(entry), + highlightTerms, + isActiveHighlight ? ActiveHighlightMarker : HighlightMarker + )} + + ))} +
+ ); + } + + return null; +}); + +const formatValue = (value: JsonValue): string => { + if (typeof value === 'string') { + return value; + } + + return stringify(value); +}; + +const CommaSeparatedLi = euiStyled.li` + display: inline; + &:not(:last-child) { + margin-right: 1ex; + &::after { + content: ','; + } + } +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx index 51488f088e75a..9b039b7f2f3ba 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_column.tsx @@ -39,7 +39,9 @@ export const LogEntryColumn = euiStyled.div.attrs(() => ({ overflow: hidden; `; -export const LogEntryColumnContent = euiStyled.div` +export const LogEntryColumnContent = euiStyled.div.attrs({ + 'data-test-subj': 'LogEntryColumnContent', +})` flex: 1 0 0%; padding: 2px ${COLUMN_PADDING}px; `; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx index d6068b6e60992..5813f08497a74 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx @@ -4,85 +4,113 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; +import { render } from '@testing-library/react'; import React from 'react'; - import { EuiThemeProvider } from '../../../../../observability/public'; +import { LogFieldColumn } from '../../../../common/http_api'; import { LogEntryFieldColumn } from './log_entry_field_column'; -import { LogColumn } from '../../../../common/http_api'; describe('LogEntryFieldColumn', () => { - it('should output a
    when displaying an Array of values', () => { - const column: LogColumn = { + it('renders a single value without a wrapping list', () => { + const column: LogFieldColumn = { + columnId: 'TEST_COLUMN', + field: 'TEST_FIELD', + value: ['a'], + highlights: [], + }; + + const renderResult = render( + , + { wrapper: EuiThemeProvider } + ); + + expect(renderResult.getByTestId('LogEntryColumnContent')).toHaveTextContent(/^a$/); + expect(renderResult.queryByTestId('LogEntryFieldValues')).toBe(null); + }); + + it('renders an array of values as a list', () => { + const column: LogFieldColumn = { columnId: 'TEST_COLUMN', field: 'TEST_FIELD', value: ['a', 'b', 'c'], highlights: [], }; - const component = mount( + const renderResult = render( , - { wrappingComponent: EuiThemeProvider } as any // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36075 + { wrapper: EuiThemeProvider } ); - expect(component.exists('ul')).toBe(true); - expect( - component.containsAllMatchingElements([ -
  • a
  • , -
  • b
  • , -
  • c
  • , - ]) - ).toBe(true); + expect(renderResult.getByTestId('LogEntryFieldValues')).not.toBeEmptyDOMElement(); + expect(renderResult.getByTestId('LogEntryFieldValue-0')).toHaveTextContent('a'); + expect(renderResult.getByTestId('LogEntryFieldValue-1')).toHaveTextContent('b'); + expect(renderResult.getByTestId('LogEntryFieldValue-2')).toHaveTextContent('c'); }); - it('should output a text representation of a passed complex value', () => { - const column: LogColumn = { + it('renders a text representation of a single complex object', () => { + const column: LogFieldColumn = { columnId: 'TEST_COLUMN', field: 'TEST_FIELD', - value: { - lat: 1, - lon: 2, - }, + value: [ + { + lat: 1, + lon: 2, + }, + ], highlights: [], }; - const component = mount( + const renderResult = render( , - { wrappingComponent: EuiThemeProvider } as any // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36075 + { wrapper: EuiThemeProvider } ); - expect(component.text()).toEqual('{"lat":1,"lon":2}'); + expect(renderResult.getByTestId('LogEntryColumnContent')).toHaveTextContent( + '{"lat":1,"lon":2}' + ); }); - it('should output just text when passed a non-Array', () => { - const column: LogColumn = { + it('renders text representations of a multiple complex objects', () => { + const column: LogFieldColumn = { columnId: 'TEST_COLUMN', field: 'TEST_FIELD', - value: 'foo', + value: [ + { + lat: 1, + lon: 2, + }, + [3, 4], + ], highlights: [], }; - const component = mount( + const renderResult = render( , - { wrappingComponent: EuiThemeProvider } as any // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36075 + { wrapper: EuiThemeProvider } ); - expect(component.exists('ul')).toBe(false); - expect(component.text()).toEqual('foo'); + expect(renderResult.getByTestId('LogEntryFieldValues')).not.toBeEmptyDOMElement(); + expect(renderResult.getByTestId('LogEntryFieldValue-0')).toHaveTextContent('{"lat":1,"lon":2}'); + expect(renderResult.getByTestId('LogEntryFieldValue-1')).toHaveTextContent('[3,4]'); }); }); diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx index 4e9611f7a8d2f..cdffe3742b534 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import stringify from 'json-stable-stringify'; -import React, { useMemo } from 'react'; - +import React from 'react'; import { euiStyled } from '../../../../../observability/public'; +import { LogColumn } from '../../../../common/http_api'; import { isFieldColumn, isHighlightFieldColumn } from '../../../utils/log_entry'; -import { ActiveHighlightMarker, highlightFieldValue, HighlightMarker } from './highlighting'; +import { FieldValue } from './field_value'; import { LogEntryColumnContent } from './log_entry_column'; -import { LogColumn } from '../../../../common/http_api'; import { longWrappedContentStyle, preWrappedContentStyle, @@ -32,44 +30,20 @@ export const LogEntryFieldColumn: React.FunctionComponent { - const value = useMemo(() => { - if (isFieldColumn(columnValue)) { - return columnValue.value; - } + if (isFieldColumn(columnValue)) { + return ( + + + + ); + } else { return null; - }, [columnValue]); - const formattedValue = Array.isArray(value) ? ( -
      - {value.map((entry, i) => ( - - {highlightFieldValue( - entry, - isHighlightFieldColumn(firstHighlight) ? firstHighlight.highlights : [], - isActiveHighlight ? ActiveHighlightMarker : HighlightMarker - )} - - ))} -
    - ) : ( - highlightFieldValue( - typeof value === 'string' ? value : stringify(value), - isHighlightFieldColumn(firstHighlight) ? firstHighlight.highlights : [], - isActiveHighlight ? ActiveHighlightMarker : HighlightMarker - ) - ); - - return {formattedValue}; -}; - -const CommaSeparatedLi = euiStyled.li` - display: inline; - &:not(:last-child) { - margin-right: 1ex; - &::after { - content: ','; - } } -`; +}; interface LogEntryColumnContentProps { wrapMode: WrapMode; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx new file mode 100644 index 0000000000000..b9871cc3b36f4 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { EuiThemeProvider } from '../../../../../observability/public'; +import { LogMessageColumn } from '../../../../common/http_api'; +import { LogEntryMessageColumn } from './log_entry_message_column'; + +describe('LogEntryMessageColumn', () => { + it('renders a single scalar field value without a wrapping list', () => { + const column: LogMessageColumn = { + columnId: 'TEST_COLUMN', + message: [{ field: 'TEST_FIELD', value: ['VALUE'], highlights: [] }], + }; + + const renderResult = render( + , + { wrapper: EuiThemeProvider } + ); + + expect(renderResult.getByTestId('LogEntryColumnContent')).toHaveTextContent(/^VALUE$/); + expect(renderResult.queryByTestId('LogEntryFieldValues')).toBe(null); + }); + + it('renders a single array of scalar field values as a list', () => { + const column: LogMessageColumn = { + columnId: 'TEST_COLUMN', + message: [{ field: 'TEST_FIELD', value: ['VALUE_1', 'VALUE_2'], highlights: [] }], + }; + + const renderResult = render( + , + { wrapper: EuiThemeProvider } + ); + + expect(renderResult.getByTestId('LogEntryFieldValues')).not.toBeEmptyDOMElement(); + expect(renderResult.getByTestId('LogEntryFieldValue-0')).toHaveTextContent('VALUE_1'); + expect(renderResult.getByTestId('LogEntryFieldValue-1')).toHaveTextContent('VALUE_2'); + }); + + it('renders a complex message with an array of complex field values', () => { + const column: LogMessageColumn = { + columnId: 'TEST_COLUMN', + message: [ + { constant: 'CONSTANT_1' }, + { field: 'TEST_FIELD', value: [{ lat: 1, lon: 2 }, 'VALUE_2'], highlights: [] }, + { constant: 'CONSTANT_2' }, + ], + }; + + const renderResult = render( + , + { wrapper: EuiThemeProvider } + ); + + expect(renderResult.getByTestId('LogEntryColumnContent')).toHaveTextContent( + /^CONSTANT_1.*{"lat":1,"lon":2}.*VALUE_2.*CONSTANT_2$/ + ); + expect(renderResult.getByTestId('LogEntryFieldValues')).not.toBeEmptyDOMElement(); + expect(renderResult.getByTestId('LogEntryFieldValue-0')).toHaveTextContent('{"lat":1,"lon":2}'); + expect(renderResult.getByTestId('LogEntryFieldValue-1')).toHaveTextContent('VALUE_2'); + }); +}); diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx index f83a0a222d3dc..b7ff6b1409bd5 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx @@ -5,17 +5,16 @@ */ import React, { memo, useMemo } from 'react'; -import stringify from 'json-stable-stringify'; - import { euiStyled } from '../../../../../observability/public'; +import { LogColumn, LogMessagePart } from '../../../../common/http_api'; import { isConstantSegment, isFieldSegment, + isHighlightFieldSegment, isHighlightMessageColumn, isMessageColumn, - isHighlightFieldSegment, } from '../../../utils/log_entry'; -import { ActiveHighlightMarker, highlightFieldValue, HighlightMarker } from './highlighting'; +import { FieldValue } from './field_value'; import { LogEntryColumnContent } from './log_entry_column'; import { longWrappedContentStyle, @@ -23,7 +22,6 @@ import { unwrappedContentStyle, WrapMode, } from './text_styles'; -import { LogColumn, LogMessagePart } from '../../../../common/http_api'; interface LogEntryMessageColumnProps { columnValue: LogColumn; @@ -65,10 +63,10 @@ const formatMessageSegments = ( highlights: LogColumn[], isActiveHighlight: boolean ) => - messageSegments.map((messageSegment, index) => - formatMessageSegment( - messageSegment, - highlights.map((highlight) => { + messageSegments.map((messageSegment, index) => { + if (isFieldSegment(messageSegment)) { + // we only support one highlight for now + const [firstHighlight = []] = highlights.map((highlight) => { if (isHighlightMessageColumn(highlight)) { const segment = highlight.message[index]; if (isHighlightFieldSegment(segment)) { @@ -76,30 +74,19 @@ const formatMessageSegments = ( } } return []; - }), - isActiveHighlight - ) - ); + }); -const formatMessageSegment = ( - messageSegment: LogMessagePart, - [firstHighlight = []]: string[][], // we only support one highlight for now - isActiveHighlight: boolean -): React.ReactNode => { - if (isFieldSegment(messageSegment)) { - const value = - typeof messageSegment.value === 'string' - ? messageSegment.value - : stringify(messageSegment.value); - - return highlightFieldValue( - value, - firstHighlight, - isActiveHighlight ? ActiveHighlightMarker : HighlightMarker - ); - } else if (isConstantSegment(messageSegment)) { - return messageSegment.constant; - } + return ( + + ); + } else if (isConstantSegment(messageSegment)) { + return messageSegment.constant; + } - return 'failed to format message'; -}; + return 'failed to format message'; + }); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx index 7bcc05280994c..84d7e198636e9 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx @@ -86,7 +86,7 @@ export const CategoryExampleMessage: React.FunctionComponent<{ = ({ = ({ columnValue={{ columnId: datasetColumnId, field: 'event.dataset', - value: humanFriendlyDataset, + value: [humanFriendlyDataset], highlights: [], }} highlights={noHighlights} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx index 6621b110a6dfd..8023e3bf7da62 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx @@ -45,8 +45,8 @@ export const Map: React.FC = ({ const sortedNodes = sortNodes(options.sort, nodes); const map = nodesToWaffleMap(sortedNodes); return ( - - {({ measureRef, content: { width = 0, height = 0 } }) => { + + {({ measureRef, bounds: { width = 0, height = 0 } }) => { const groupsWithLayout = applyWaffleMapLayout(map, width, height); return ( { initLogEntriesSummaryHighlightsRoute(libs); initLogEntriesItemRoute(libs); initMetricExplorerRoute(libs); + initMetricsAPIRoute(libs); initMetadataRoute(libs); initInventoryMetaRoute(libs); initLogSourceConfigurationRoutes(libs); diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 1ecae84c54ffb..40ff69f78cee2 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -15,6 +15,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../pl import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { PluginSetupContract as AlertingPluginContract } from '../../../../../alerts/server'; import { MlPluginSetup } from '../../../../../ml/server'; +import { JsonArray, JsonValue } from '../../../../common/typed_json'; export interface InfraServerPluginDeps { home: HomeServerPluginSetup; @@ -111,7 +112,10 @@ export type SearchHit = SearchResponse['hits']['hits'][0]; export interface SortedSearchHit extends SearchHit { sort: any[]; _source: { - [field: string]: any; + [field: string]: JsonValue; + }; + fields: { + [field: string]: JsonArray; }; } diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index c5b667fb20538..9309ad85a3570 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -4,18 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable @typescript-eslint/no-empty-interface */ - import { timeMilliseconds } from 'd3-time'; +import { fold, map } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; import * as runtimeTypes from 'io-ts'; import { compact, first } from 'lodash'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { map, fold } from 'fp-ts/lib/Either'; -import { identity, constant } from 'fp-ts/lib/function'; import { RequestHandlerContext } from 'src/core/server'; -import { JsonValue } from '../../../../common/typed_json'; +import { JsonArray } from '../../../../common/typed_json'; import { LogEntriesAdapter, + LogItemHit, LogEntriesParams, LogEntryDocument, LogEntryQuery, @@ -28,13 +27,6 @@ import { KibanaFramework } from '../framework/kibana_framework_adapter'; const TIMESTAMP_FORMAT = 'epoch_millis'; -interface LogItemHit { - _index: string; - _id: string; - fields: { [key: string]: [value: unknown] }; - sort: [number, number]; -} - export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { constructor(private readonly framework: KibanaFramework) {} @@ -231,13 +223,14 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]): LogEntryDocument[] { return hits.map((hit) => { - const logFields = fields.reduce<{ [fieldName: string]: JsonValue }>( - (flattenedFields, field) => { - if (field in hit.fields) { - flattenedFields[field] = hit.fields[field][0]; - } - return flattenedFields; - }, + const logFields = fields.reduce<{ [fieldName: string]: JsonArray }>( + (flattenedFields, field) => + field in hit.fields + ? { + ...flattenedFields, + [field]: hit.fields[field], + } + : flattenedFields, {} ); @@ -338,8 +331,9 @@ const LogSummaryDateRangeBucketRuntimeType = runtimeTypes.intersection([ }), ]); -export interface LogSummaryDateRangeBucket - extends runtimeTypes.TypeOf {} +export type LogSummaryDateRangeBucket = runtimeTypes.TypeOf< + typeof LogSummaryDateRangeBucketRuntimeType +>; const LogSummaryResponseRuntimeType = runtimeTypes.type({ aggregations: runtimeTypes.type({ @@ -349,5 +343,4 @@ const LogSummaryResponseRuntimeType = runtimeTypes.type({ }), }); -export interface LogSummaryResponse - extends runtimeTypes.TypeOf {} +export type LogSummaryResponse = runtimeTypes.TypeOf; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.test.ts index 367ae6a0cae89..e7e381dc7f8f6 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.test.ts @@ -13,251 +13,290 @@ describe('Filebeat Rules', () => { describe('in ECS format', () => { test('Apache2 Access', () => { const flattenedDocument = { - '@timestamp': '2016-12-26T16:22:13.000Z', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'apache.access', - 'event.module': 'apache', - 'fileset.name': 'access', - 'http.request.method': 'GET', - 'http.request.referrer': '-', - 'http.response.body.bytes': 499, - 'http.response.status_code': 404, - 'http.version': '1.1', - 'input.type': 'log', - 'log.offset': 73, - 'service.type': 'apache', - 'source.address': '192.168.33.1', - 'source.ip': '192.168.33.1', - 'url.original': '/hello', - 'user.name': '-', - 'user_agent.device': 'Other', - 'user_agent.major': '50', - 'user_agent.minor': '0', - 'user_agent.name': 'Firefox', - 'user_agent.original': + '@timestamp': ['2016-12-26T16:22:13.000Z'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['apache.access'], + 'event.module': ['apache'], + 'fileset.name': ['access'], + 'http.request.method': ['GET'], + 'http.request.referrer': ['-'], + 'http.response.body.bytes': [499], + 'http.response.status_code': [404], + 'http.version': ['1.1'], + 'input.type': ['log'], + 'log.offset': [73], + 'service.type': ['apache'], + 'source.address': ['192.168.33.1'], + 'source.ip': ['192.168.33.1'], + 'url.original': ['/hello'], + 'user.name': ['-'], + 'user_agent.device': ['Other'], + 'user_agent.major': ['50'], + 'user_agent.minor': ['0'], + 'user_agent.name': ['Firefox'], + 'user_agent.original': [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:50.0) Gecko/20100101 Firefox/50.0', - 'user_agent.os.full_name': 'Mac OS X 10.12', - 'user_agent.os.major': '10', - 'user_agent.os.minor': '12', - 'user_agent.os.name': 'Mac OS X', + ], + 'user_agent.os.full_name': ['Mac OS X 10.12'], + 'user_agent.os.major': ['10'], + 'user_agent.os.minor': ['12'], + 'user_agent.os.name': ['Mac OS X'], }; const highlights = { 'http.request.method': ['GET'], }; expect(format(flattenedDocument, highlights)).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "event.module", - "highlights": Array [], - "value": "apache", - }, - Object { - "constant": "][access] ", - }, - Object { - "field": "source.ip", - "highlights": Array [], - "value": "192.168.33.1", - }, - Object { - "constant": " ", - }, - Object { - "field": "user.name", - "highlights": Array [], - "value": "-", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "http.request.method", - "highlights": Array [ - "GET", - ], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "url.original", - "highlights": Array [], - "value": "/hello", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "http.version", - "highlights": Array [], - "value": "1.1", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "http.response.status_code", - "highlights": Array [], - "value": "404", - }, - Object { - "constant": " ", - }, - Object { - "field": "http.response.body.bytes", - "highlights": Array [], - "value": "499", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "event.module", + "highlights": Array [], + "value": Array [ + "apache", + ], + }, + Object { + "constant": "][access] ", + }, + Object { + "field": "source.ip", + "highlights": Array [], + "value": Array [ + "192.168.33.1", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "user.name", + "highlights": Array [], + "value": Array [ + "-", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "http.request.method", + "highlights": Array [ + "GET", + ], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "url.original", + "highlights": Array [], + "value": Array [ + "/hello", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "http.version", + "highlights": Array [], + "value": Array [ + "1.1", + ], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "http.response.status_code", + "highlights": Array [], + "value": Array [ + 404, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "http.response.body.bytes", + "highlights": Array [], + "value": Array [ + 499, + ], + }, + ] + `); }); test('Apache2 Error', () => { const flattenedDocument = { - '@timestamp': '2016-12-26T16:22:08.000Z', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'apache.error', - 'event.module': 'apache', - 'fileset.name': 'error', - 'input.type': 'log', - 'log.level': 'error', - 'log.offset': 0, - message: 'File does not exist: /var/www/favicon.ico', - 'service.type': 'apache', - 'source.address': '192.168.33.1', - 'source.ip': '192.168.33.1', + '@timestamp': ['2016-12-26T16:22:08.000Z'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['apache.error'], + 'event.module': ['apache'], + 'fileset.name': ['error'], + 'input.type': ['log'], + 'log.level': ['error'], + 'log.offset': [0], + message: ['File does not exist: /var/www/favicon.ico'], + 'service.type': ['apache'], + 'source.address': ['192.168.33.1'], + 'source.ip': ['192.168.33.1'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[apache][", - }, - Object { - "field": "log.level", - "highlights": Array [], - "value": "error", - }, - Object { - "constant": "] ", - }, - Object { - "field": "message", - "highlights": Array [], - "value": "File does not exist: /var/www/favicon.ico", - }, -] -`); + Array [ + Object { + "constant": "[apache][", + }, + Object { + "field": "log.level", + "highlights": Array [], + "value": Array [ + "error", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "message", + "highlights": Array [], + "value": Array [ + "File does not exist: /var/www/favicon.ico", + ], + }, + ] + `); }); }); describe('in pre-ECS format', () => { test('Apache2 Access', () => { const flattenedDocument = { - 'apache2.access': true, - 'apache2.access.remote_ip': '192.168.1.42', - 'apache2.access.user_name': 'admin', - 'apache2.access.method': 'GET', - 'apache2.access.url': '/faqs', - 'apache2.access.http_version': '1.1', - 'apache2.access.response_code': '200', - 'apache2.access.body_sent.bytes': 1024, + 'apache2.access.remote_ip': ['192.168.1.42'], + 'apache2.access.user_name': ['admin'], + 'apache2.access.method': ['GET'], + 'apache2.access.url': ['/faqs'], + 'apache2.access.http_version': ['1.1'], + 'apache2.access.response_code': ['200'], + 'apache2.access.body_sent.bytes': [1024], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[apache][access] ", - }, - Object { - "field": "apache2.access.remote_ip", - "highlights": Array [], - "value": "192.168.1.42", - }, - Object { - "constant": " ", - }, - Object { - "field": "apache2.access.user_name", - "highlights": Array [], - "value": "admin", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "apache2.access.method", - "highlights": Array [], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "apache2.access.url", - "highlights": Array [], - "value": "/faqs", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "apache2.access.http_version", - "highlights": Array [], - "value": "1.1", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "apache2.access.response_code", - "highlights": Array [], - "value": "200", - }, - Object { - "constant": " ", - }, - Object { - "field": "apache2.access.body_sent.bytes", - "highlights": Array [], - "value": "1024", - }, -] -`); + Array [ + Object { + "constant": "[apache][access] ", + }, + Object { + "field": "apache2.access.remote_ip", + "highlights": Array [], + "value": Array [ + "192.168.1.42", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "apache2.access.user_name", + "highlights": Array [], + "value": Array [ + "admin", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "apache2.access.method", + "highlights": Array [], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "apache2.access.url", + "highlights": Array [], + "value": Array [ + "/faqs", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "apache2.access.http_version", + "highlights": Array [], + "value": Array [ + "1.1", + ], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "apache2.access.response_code", + "highlights": Array [], + "value": Array [ + "200", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "apache2.access.body_sent.bytes", + "highlights": Array [], + "value": Array [ + 1024, + ], + }, + ] + `); }); test('Apache2 Error', () => { const flattenedDocument = { - 'apache2.error.message': + 'apache2.error.message': [ 'AH00489: Apache/2.4.18 (Ubuntu) configured -- resuming normal operations', - 'apache2.error.level': 'notice', + ], + 'apache2.error.level': ['notice'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[apache][", - }, - Object { - "field": "apache2.error.level", - "highlights": Array [], - "value": "notice", - }, - Object { - "constant": "] ", - }, - Object { - "field": "apache2.error.message", - "highlights": Array [], - "value": "AH00489: Apache/2.4.18 (Ubuntu) configured -- resuming normal operations", - }, -] -`); + Array [ + Object { + "constant": "[apache][", + }, + Object { + "field": "apache2.error.level", + "highlights": Array [], + "value": Array [ + "notice", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "apache2.error.message", + "highlights": Array [], + "value": Array [ + "AH00489: Apache/2.4.18 (Ubuntu) configured -- resuming normal operations", + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.ts index fe7ebffe91329..3698753ef42ea 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.ts @@ -8,7 +8,7 @@ export const filebeatApache2Rules = [ { // pre-ECS when: { - exists: ['apache2.access'], + existsPrefix: ['apache2.access'], }, format: [ { diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_auditd.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_auditd.test.ts index aa490c595d9fd..4481ff434802f 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_auditd.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_auditd.test.ts @@ -13,345 +13,605 @@ describe('Filebeat Rules', () => { describe('in ECS format', () => { test('auditd log with outcome', () => { const flattenedDocument = { - '@timestamp': '2016-12-07T02:17:21.515Z', - 'auditd.log': { - addr: '96.241.146.97', - cipher: 'chacha20-poly1305@openssh.com', - direction: 'from-server', - ksize: '512', - laddr: '10.142.0.2', - lport: '22', - pfs: 'curve25519-sha256@libssh.org', - rport: '63927', - sequence: 406, - ses: '4294967295', - spid: '1299', - subj: 'system_u:system_r:sshd_t:s0-s0:c0.c1023', - }, - 'ecs.version': '1.0.0-beta2', - 'event.action': 'crypto_session', - 'event.dataset': 'auditd.log', - 'event.module': 'auditd', - 'event.outcome': 'success', - 'fileset.name': 'log', - 'input.type': 'log', - 'log.offset': 783, - message: 'op=start', - process: { executable: '/usr/sbin/sshd', pid: 1298 }, - 'service.type': 'auditd', - user: { 'audit.id': '4294967295', id: '0', 'saved.id': '74' }, + '@timestamp': ['2016-12-07T02:17:21.515Z'], + 'auditd.log.addr': ['96.241.146.97'], + 'auditd.log.cipher': ['chacha20-poly1305@openssh.com'], + 'auditd.log.direction': ['from-server'], + 'auditd.log.ksize': ['512'], + 'auditd.log.laddr': ['10.142.0.2'], + 'auditd.log.lport': ['22'], + 'auditd.log.pfs': ['curve25519-sha256@libssh.org'], + 'auditd.log.rport': ['63927'], + 'auditd.log.sequence': [406], + 'auditd.log.ses': ['4294967295'], + 'auditd.log.spid': ['1299'], + 'auditd.log.subj': ['system_u:system_r:sshd_t:s0-s0:c0.c1023'], + 'ecs.version': ['1.0.0-beta2'], + 'event.action': ['crypto_session'], + 'event.dataset': ['auditd.log'], + 'event.module': ['auditd'], + 'event.outcome': ['success'], + 'fileset.name': ['log'], + 'input.type': ['log'], + 'log.offset': [783], + message: ['op=start'], + 'process.executable': ['/usr/sbin/sshd'], + 'process.pid': [1298], + 'service.type': ['auditd'], + 'user.audit.id': ['4294967295'], + 'user.id': ['0'], + 'user.saved.id': ['74'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[AuditD][", - }, - Object { - "field": "event.action", - "highlights": Array [], - "value": "crypto_session", - }, - Object { - "constant": "]", - }, - Object { - "constant": " ", - }, - Object { - "field": "event.outcome", - "highlights": Array [], - "value": "success", - }, - Object { - "constant": " ", - }, - Object { - "constant": "user", - }, - Object { - "constant": "=", - }, - Object { - "field": "user", - "highlights": Array [], - "value": "{\\"audit.id\\":\\"4294967295\\",\\"id\\":\\"0\\",\\"saved.id\\":\\"74\\"}", - }, - Object { - "constant": " ", - }, - Object { - "constant": "process", - }, - Object { - "constant": "=", - }, - Object { - "field": "process", - "highlights": Array [], - "value": "{\\"executable\\":\\"/usr/sbin/sshd\\",\\"pid\\":1298}", - }, - Object { - "constant": " ", - }, - Object { - "field": "auditd.log", - "highlights": Array [], - "value": "{\\"addr\\":\\"96.241.146.97\\",\\"cipher\\":\\"chacha20-poly1305@openssh.com\\",\\"direction\\":\\"from-server\\",\\"ksize\\":\\"512\\",\\"laddr\\":\\"10.142.0.2\\",\\"lport\\":\\"22\\",\\"pfs\\":\\"curve25519-sha256@libssh.org\\",\\"rport\\":\\"63927\\",\\"sequence\\":406,\\"ses\\":\\"4294967295\\",\\"spid\\":\\"1299\\",\\"subj\\":\\"system_u:system_r:sshd_t:s0-s0:c0.c1023\\"}", - }, - Object { - "constant": " ", - }, - Object { - "field": "message", - "highlights": Array [], - "value": "op=start", - }, -] -`); + Array [ + Object { + "constant": "[AuditD][", + }, + Object { + "field": "event.action", + "highlights": Array [], + "value": Array [ + "crypto_session", + ], + }, + Object { + "constant": "]", + }, + Object { + "constant": " ", + }, + Object { + "field": "event.outcome", + "highlights": Array [], + "value": Array [ + "success", + ], + }, + Object { + "constant": " ", + }, + Object { + "constant": "user", + }, + Object { + "constant": "=", + }, + Object { + "field": "user.audit.id", + "highlights": Array [], + "value": Array [ + "4294967295", + ], + }, + Object { + "field": "user.id", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "user.saved.id", + "highlights": Array [], + "value": Array [ + "74", + ], + }, + Object { + "constant": " ", + }, + Object { + "constant": "process", + }, + Object { + "constant": "=", + }, + Object { + "field": "process.executable", + "highlights": Array [], + "value": Array [ + "/usr/sbin/sshd", + ], + }, + Object { + "field": "process.pid", + "highlights": Array [], + "value": Array [ + 1298, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "auditd.log.addr", + "highlights": Array [], + "value": Array [ + "96.241.146.97", + ], + }, + Object { + "field": "auditd.log.cipher", + "highlights": Array [], + "value": Array [ + "chacha20-poly1305@openssh.com", + ], + }, + Object { + "field": "auditd.log.direction", + "highlights": Array [], + "value": Array [ + "from-server", + ], + }, + Object { + "field": "auditd.log.ksize", + "highlights": Array [], + "value": Array [ + "512", + ], + }, + Object { + "field": "auditd.log.laddr", + "highlights": Array [], + "value": Array [ + "10.142.0.2", + ], + }, + Object { + "field": "auditd.log.lport", + "highlights": Array [], + "value": Array [ + "22", + ], + }, + Object { + "field": "auditd.log.pfs", + "highlights": Array [], + "value": Array [ + "curve25519-sha256@libssh.org", + ], + }, + Object { + "field": "auditd.log.rport", + "highlights": Array [], + "value": Array [ + "63927", + ], + }, + Object { + "field": "auditd.log.sequence", + "highlights": Array [], + "value": Array [ + 406, + ], + }, + Object { + "field": "auditd.log.ses", + "highlights": Array [], + "value": Array [ + "4294967295", + ], + }, + Object { + "field": "auditd.log.spid", + "highlights": Array [], + "value": Array [ + "1299", + ], + }, + Object { + "field": "auditd.log.subj", + "highlights": Array [], + "value": Array [ + "system_u:system_r:sshd_t:s0-s0:c0.c1023", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "message", + "highlights": Array [], + "value": Array [ + "op=start", + ], + }, + ] + `); }); test('auditd log without outcome', () => { const flattenedDocument = { - '@timestamp': '2017-01-31T20:17:14.891Z', - 'auditd.log': { - a0: '9', - a1: '7f564b2672a0', - a2: 'b8', - a3: '0', - exit: '184', - items: '0', - sequence: 18877199, - ses: '4294967295', - success: 'yes', - syscall: '44', - tty: '(none)', - }, - 'ecs.version': '1.0.0-beta2', - 'event.action': 'syscall', - 'event.dataset': 'auditd.log', - 'event.module': 'auditd', - 'fileset.name': 'log', - 'host.architecture': 'x86_64', - 'input.type': 'log', - 'log.offset': 174, - process: { - executable: '/usr/libexec/strongswan/charon (deleted)', - name: 'charon', - pid: 1281, - ppid: 1240, - }, - 'service.type': 'auditd', - user: { - 'audit.id': '4294967295', - 'effective.group.id': '0', - 'effective.id': '0', - 'filesystem.group.id': '0', - 'filesystem.id': '0', - 'group.id': '0', - id: '0', - 'saved.group.id': '0', - 'saved.id': '0', - }, + '@timestamp': ['2017-01-31T20:17:14.891Z'], + 'auditd.log.a0': ['9'], + 'auditd.log.a1': ['7f564b2672a0'], + 'auditd.log.a2': ['b8'], + 'auditd.log.a3': ['0'], + 'auditd.log.exit': ['184'], + 'auditd.log.items': ['0'], + 'auditd.log.sequence': [18877199], + 'auditd.log.ses': ['4294967295'], + 'auditd.log.success': ['yes'], + 'auditd.log.syscall': ['44'], + 'auditd.log.tty': ['(none)'], + 'ecs.version': ['1.0.0-beta2'], + 'event.action': ['syscall'], + 'event.dataset': ['auditd.log'], + 'event.module': ['auditd'], + 'fileset.name': ['log'], + 'host.architecture': ['x86_64'], + 'input.type': ['log'], + 'log.offset': [174], + 'process.executable': ['/usr/libexec/strongswan/charon (deleted)'], + 'process.name': ['charon'], + 'process.pid': [1281], + 'process.ppid': [1240], + 'service.type': ['auditd'], + 'user.audit.id': ['4294967295'], + 'user.effective.group.id': ['0'], + 'user.effective.id': ['0'], + 'user.filesystem.group.id': ['0'], + 'user.filesystem.id': ['0'], + 'user.group.id': ['0'], + 'user.id': ['0'], + 'user.saved.group.id': ['0'], + 'user.saved.id': ['0'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[AuditD][", - }, - Object { - "field": "event.action", - "highlights": Array [], - "value": "syscall", - }, - Object { - "constant": "]", - }, - Object { - "constant": " ", - }, - Object { - "constant": "user", - }, - Object { - "constant": "=", - }, - Object { - "field": "user", - "highlights": Array [], - "value": "{\\"audit.id\\":\\"4294967295\\",\\"effective.group.id\\":\\"0\\",\\"effective.id\\":\\"0\\",\\"filesystem.group.id\\":\\"0\\",\\"filesystem.id\\":\\"0\\",\\"group.id\\":\\"0\\",\\"id\\":\\"0\\",\\"saved.group.id\\":\\"0\\",\\"saved.id\\":\\"0\\"}", - }, - Object { - "constant": " ", - }, - Object { - "constant": "process", - }, - Object { - "constant": "=", - }, - Object { - "field": "process", - "highlights": Array [], - "value": "{\\"executable\\":\\"/usr/libexec/strongswan/charon (deleted)\\",\\"name\\":\\"charon\\",\\"pid\\":1281,\\"ppid\\":1240}", - }, - Object { - "constant": " ", - }, - Object { - "field": "auditd.log", - "highlights": Array [], - "value": "{\\"a0\\":\\"9\\",\\"a1\\":\\"7f564b2672a0\\",\\"a2\\":\\"b8\\",\\"a3\\":\\"0\\",\\"exit\\":\\"184\\",\\"items\\":\\"0\\",\\"sequence\\":18877199,\\"ses\\":\\"4294967295\\",\\"success\\":\\"yes\\",\\"syscall\\":\\"44\\",\\"tty\\":\\"(none)\\"}", - }, - Object { - "constant": " ", - }, - Object { - "field": "message", - "highlights": Array [], - "value": "undefined", - }, -] -`); + Array [ + Object { + "constant": "[AuditD][", + }, + Object { + "field": "event.action", + "highlights": Array [], + "value": Array [ + "syscall", + ], + }, + Object { + "constant": "]", + }, + Object { + "constant": " ", + }, + Object { + "constant": "user", + }, + Object { + "constant": "=", + }, + Object { + "field": "user.audit.id", + "highlights": Array [], + "value": Array [ + "4294967295", + ], + }, + Object { + "field": "user.effective.group.id", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "user.effective.id", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "user.filesystem.group.id", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "user.filesystem.id", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "user.group.id", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "user.id", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "user.saved.group.id", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "user.saved.id", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "constant": " ", + }, + Object { + "constant": "process", + }, + Object { + "constant": "=", + }, + Object { + "field": "process.executable", + "highlights": Array [], + "value": Array [ + "/usr/libexec/strongswan/charon (deleted)", + ], + }, + Object { + "field": "process.name", + "highlights": Array [], + "value": Array [ + "charon", + ], + }, + Object { + "field": "process.pid", + "highlights": Array [], + "value": Array [ + 1281, + ], + }, + Object { + "field": "process.ppid", + "highlights": Array [], + "value": Array [ + 1240, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "auditd.log.a0", + "highlights": Array [], + "value": Array [ + "9", + ], + }, + Object { + "field": "auditd.log.a1", + "highlights": Array [], + "value": Array [ + "7f564b2672a0", + ], + }, + Object { + "field": "auditd.log.a2", + "highlights": Array [], + "value": Array [ + "b8", + ], + }, + Object { + "field": "auditd.log.a3", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "auditd.log.exit", + "highlights": Array [], + "value": Array [ + "184", + ], + }, + Object { + "field": "auditd.log.items", + "highlights": Array [], + "value": Array [ + "0", + ], + }, + Object { + "field": "auditd.log.sequence", + "highlights": Array [], + "value": Array [ + 18877199, + ], + }, + Object { + "field": "auditd.log.ses", + "highlights": Array [], + "value": Array [ + "4294967295", + ], + }, + Object { + "field": "auditd.log.success", + "highlights": Array [], + "value": Array [ + "yes", + ], + }, + Object { + "field": "auditd.log.syscall", + "highlights": Array [], + "value": Array [ + "44", + ], + }, + Object { + "field": "auditd.log.tty", + "highlights": Array [], + "value": Array [ + "(none)", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "message", + "highlights": Array [], + "value": Array [], + }, + ] + `); }); }); describe('in pre-ECS format', () => { test('auditd IPSEC rule', () => { const event = { - '@timestamp': '2017-01-31T20:17:14.891Z', - 'auditd.log.auid': '4294967295', - 'auditd.log.dst': '192.168.0.0', - 'auditd.log.dst_prefixlen': '16', - 'auditd.log.op': 'SPD-delete', - 'auditd.log.record_type': 'MAC_IPSEC_EVENT', - 'auditd.log.res': '1', - 'auditd.log.sequence': 18877201, - 'auditd.log.ses': '4294967295', - 'auditd.log.src': '192.168.2.0', - 'auditd.log.src_prefixlen': '24', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'auditd.log', - 'event.module': 'auditd', - 'fileset.name': 'log', - 'input.type': 'log', - 'log.offset': 0, + '@timestamp': ['2017-01-31T20:17:14.891Z'], + 'auditd.log.auid': ['4294967295'], + 'auditd.log.dst': ['192.168.0.0'], + 'auditd.log.dst_prefixlen': ['16'], + 'auditd.log.op': ['SPD-delete'], + 'auditd.log.record_type': ['MAC_IPSEC_EVENT'], + 'auditd.log.res': ['1'], + 'auditd.log.sequence': [18877201], + 'auditd.log.ses': ['4294967295'], + 'auditd.log.src': ['192.168.2.0'], + 'auditd.log.src_prefixlen': ['24'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['auditd.log'], + 'event.module': ['auditd'], + 'fileset.name': ['log'], + 'input.type': ['log'], + 'log.offset': [0], }; const message = format(event, {}); expect(message).toEqual([ { constant: '[AuditD][' }, - { field: 'auditd.log.record_type', highlights: [], value: 'MAC_IPSEC_EVENT' }, + { field: 'auditd.log.record_type', highlights: [], value: ['MAC_IPSEC_EVENT'] }, { constant: '] src:' }, - { field: 'auditd.log.src', highlights: [], value: '192.168.2.0' }, + { field: 'auditd.log.src', highlights: [], value: ['192.168.2.0'] }, { constant: ' dst:' }, - { field: 'auditd.log.dst', highlights: [], value: '192.168.0.0' }, + { field: 'auditd.log.dst', highlights: [], value: ['192.168.0.0'] }, { constant: ' op:' }, - { field: 'auditd.log.op', highlights: [], value: 'SPD-delete' }, + { field: 'auditd.log.op', highlights: [], value: ['SPD-delete'] }, ]); }); test('AuditD SYSCALL rule', () => { const event = { - '@timestamp': '2017-01-31T20:17:14.891Z', - 'auditd.log.a0': '9', - 'auditd.log.a1': '7f564b2672a0', - 'auditd.log.a2': 'b8', - 'auditd.log.a3': '0', - 'auditd.log.arch': 'x86_64', - 'auditd.log.auid': '4294967295', - 'auditd.log.comm': 'charon', - 'auditd.log.egid': '0', - 'auditd.log.euid': '0', - 'auditd.log.exe': '/usr/libexec/strongswan/charon (deleted)', - 'auditd.log.exit': '184', - 'auditd.log.fsgid': '0', - 'auditd.log.fsuid': '0', - 'auditd.log.gid': '0', - 'auditd.log.items': '0', - 'auditd.log.pid': '1281', - 'auditd.log.ppid': '1240', - 'auditd.log.record_type': 'SYSCALL', - 'auditd.log.sequence': 18877199, - 'auditd.log.ses': '4294967295', - 'auditd.log.sgid': '0', - 'auditd.log.success': 'yes', - 'auditd.log.suid': '0', - 'auditd.log.syscall': '44', - 'auditd.log.tty': '(none)', - 'auditd.log.uid': '0', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'auditd.log', - 'event.module': 'auditd', - 'fileset.name': 'log', - 'input.type': 'log', - 'log.offset': 174, + '@timestamp': ['2017-01-31T20:17:14.891Z'], + 'auditd.log.a0': ['9'], + 'auditd.log.a1': ['7f564b2672a0'], + 'auditd.log.a2': ['b8'], + 'auditd.log.a3': ['0'], + 'auditd.log.arch': ['x86_64'], + 'auditd.log.auid': ['4294967295'], + 'auditd.log.comm': ['charon'], + 'auditd.log.egid': ['0'], + 'auditd.log.euid': ['0'], + 'auditd.log.exe': ['/usr/libexec/strongswan/charon (deleted)'], + 'auditd.log.exit': ['184'], + 'auditd.log.fsgid': ['0'], + 'auditd.log.fsuid': ['0'], + 'auditd.log.gid': ['0'], + 'auditd.log.items': ['0'], + 'auditd.log.pid': ['1281'], + 'auditd.log.ppid': ['1240'], + 'auditd.log.record_type': ['SYSCALL'], + 'auditd.log.sequence': [18877199], + 'auditd.log.ses': ['4294967295'], + 'auditd.log.sgid': ['0'], + 'auditd.log.success': ['yes'], + 'auditd.log.suid': ['0'], + 'auditd.log.syscall': ['44'], + 'auditd.log.tty': ['(none)'], + 'auditd.log.uid': ['0'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['auditd.log'], + 'event.module': ['auditd'], + 'fileset.name': ['log'], + 'input.type': ['log'], + 'log.offset': [174], }; const message = format(event, {}); expect(message).toEqual([ { constant: '[AuditD][' }, - { field: 'auditd.log.record_type', highlights: [], value: 'SYSCALL' }, + { field: 'auditd.log.record_type', highlights: [], value: ['SYSCALL'] }, { constant: '] exe:' }, { field: 'auditd.log.exe', highlights: [], - value: '/usr/libexec/strongswan/charon (deleted)', + value: ['/usr/libexec/strongswan/charon (deleted)'], }, { constant: ' gid:' }, - { field: 'auditd.log.gid', highlights: [], value: '0' }, + { field: 'auditd.log.gid', highlights: [], value: ['0'] }, { constant: ' uid:' }, - { field: 'auditd.log.uid', highlights: [], value: '0' }, + { field: 'auditd.log.uid', highlights: [], value: ['0'] }, { constant: ' tty:' }, - { field: 'auditd.log.tty', highlights: [], value: '(none)' }, + { field: 'auditd.log.tty', highlights: [], value: ['(none)'] }, { constant: ' pid:' }, - { field: 'auditd.log.pid', highlights: [], value: '1281' }, + { field: 'auditd.log.pid', highlights: [], value: ['1281'] }, { constant: ' ppid:' }, - { field: 'auditd.log.ppid', highlights: [], value: '1240' }, + { field: 'auditd.log.ppid', highlights: [], value: ['1240'] }, ]); }); test('AuditD events with msg rule', () => { const event = { - '@timestamp': '2017-01-31T20:17:14.891Z', - 'auditd.log.auid': '4294967295', - 'auditd.log.record_type': 'EXAMPLE', - 'auditd.log.msg': 'some kind of message', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'auditd.log', - 'event.module': 'auditd', - 'fileset.name': 'log', - 'input.type': 'log', - 'log.offset': 174, + '@timestamp': ['2017-01-31T20:17:14.891Z'], + 'auditd.log.auid': ['4294967295'], + 'auditd.log.record_type': ['EXAMPLE'], + 'auditd.log.msg': ['some kind of message'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['auditd.log'], + 'event.module': ['auditd'], + 'fileset.name': ['log'], + 'input.type': ['log'], + 'log.offset': [174], }; const message = format(event, {}); expect(message).toEqual([ { constant: '[AuditD][' }, - { field: 'auditd.log.record_type', highlights: [], value: 'EXAMPLE' }, + { field: 'auditd.log.record_type', highlights: [], value: ['EXAMPLE'] }, { constant: '] ' }, { field: 'auditd.log.msg', highlights: [], - value: 'some kind of message', + value: ['some kind of message'], }, ]); }); test('AuditD catchall rule', () => { const event = { - '@timestamp': '2017-01-31T20:17:14.891Z', - 'auditd.log.auid': '4294967295', - 'auditd.log.record_type': 'EXAMPLE', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'auditd.log', - 'event.module': 'auditd', - 'fileset.name': 'log', - 'input.type': 'log', - 'log.offset': 174, + '@timestamp': ['2017-01-31T20:17:14.891Z'], + 'auditd.log.auid': ['4294967295'], + 'auditd.log.record_type': ['EXAMPLE'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['auditd.log'], + 'event.module': ['auditd'], + 'fileset.name': ['log'], + 'input.type': ['log'], + 'log.offset': [174], }; const message = format(event, {}); expect(message).toEqual([ { constant: '[AuditD][' }, - { field: 'auditd.log.record_type', highlights: [], value: 'EXAMPLE' }, + { field: 'auditd.log.record_type', highlights: [], value: ['EXAMPLE'] }, { constant: '] Event without message.' }, ]); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_auditd.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_auditd.ts index d2557cf1599ce..cd64697db6d1c 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_auditd.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_auditd.ts @@ -4,24 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { labelField } from './helpers'; +import { LogMessageFormattingRule } from '../rule_types'; +import { labelFieldsPrefix } from './helpers'; const commonActionField = [{ constant: '[AuditD][' }, { field: 'event.action' }, { constant: ']' }]; const commonOutcomeField = [{ constant: ' ' }, { field: 'event.outcome' }]; -export const filebeatAuditdRules = [ +export const filebeatAuditdRules: LogMessageFormattingRule[] = [ { // ECS format with outcome when: { - exists: ['ecs.version', 'event.action', 'event.outcome', 'auditd.log'], + all: [ + { exists: ['ecs.version', 'event.action', 'event.outcome'] }, + { existsPrefix: ['auditd.log'] }, + ], }, format: [ ...commonActionField, ...commonOutcomeField, - ...labelField('user', 'user'), - ...labelField('process', 'process'), + ...labelFieldsPrefix('user', 'user'), + ...labelFieldsPrefix('process', 'process'), { constant: ' ' }, - { field: 'auditd.log' }, + { fieldsPrefix: 'auditd.log' }, { constant: ' ' }, { field: 'message' }, ], @@ -29,14 +33,14 @@ export const filebeatAuditdRules = [ { // ECS format without outcome when: { - exists: ['ecs.version', 'event.action', 'auditd.log'], + all: [{ exists: ['ecs.version', 'event.action'] }, { existsPrefix: ['auditd.log'] }], }, format: [ ...commonActionField, - ...labelField('user', 'user'), - ...labelField('process', 'process'), + ...labelFieldsPrefix('user', 'user'), + ...labelFieldsPrefix('process', 'process'), { constant: ' ' }, - { field: 'auditd.log' }, + { fieldsPrefix: 'auditd.log' }, { constant: ' ' }, { field: 'message' }, ], @@ -44,10 +48,10 @@ export const filebeatAuditdRules = [ { // pre-ECS IPSEC_EVENT Rule when: { - exists: ['auditd.log.record_type', 'auditd.log.src', 'auditd.log.dst', 'auditd.log.op'], - values: { - 'auditd.log.record_type': 'MAC_IPSEC_EVENT', - }, + all: [ + { exists: ['auditd.log.record_type', 'auditd.log.src', 'auditd.log.dst', 'auditd.log.op'] }, + { values: { 'auditd.log.record_type': 'MAC_IPSEC_EVENT' } }, + ], }, format: [ { constant: '[AuditD][' }, @@ -63,18 +67,20 @@ export const filebeatAuditdRules = [ { // pre-ECS SYSCALL Rule when: { - exists: [ - 'auditd.log.record_type', - 'auditd.log.exe', - 'auditd.log.gid', - 'auditd.log.uid', - 'auditd.log.tty', - 'auditd.log.pid', - 'auditd.log.ppid', + all: [ + { + exists: [ + 'auditd.log.record_type', + 'auditd.log.exe', + 'auditd.log.gid', + 'auditd.log.uid', + 'auditd.log.tty', + 'auditd.log.pid', + 'auditd.log.ppid', + ], + }, + { values: { 'auditd.log.record_type': 'SYSCALL' } }, ], - values: { - 'auditd.log.record_type': 'SYSCALL', - }, }, format: [ { constant: '[AuditD][' }, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_haproxy.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_haproxy.test.ts index 752b61684887e..40e8ea0fad857 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_haproxy.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_haproxy.test.ts @@ -13,779 +13,917 @@ describe('Filebeat Rules', () => { describe('in ECS format', () => { test('haproxy default log', () => { const flattenedDocument = { - 'destination.ip': '1.2.3.4', - 'destination.port': 5000, - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'haproxy.log', - 'event.module': 'haproxy', - 'fileset.name': 'log', - 'haproxy.frontend_name': 'main', - 'haproxy.mode': 'HTTP', - 'haproxy.source': '1.2.3.4', - 'input.type': 'log', - 'log.offset': 0, - 'process.name': 'haproxy', - 'process.pid': 24551, - 'service.type': 'haproxy', - 'source.address': '1.2.3.4', - 'source.geo.continent_name': 'North America', - 'source.geo.country_iso_code': 'US', - 'source.geo.location.lat': 37.751, - 'source.geo.location.lon': -97.822, - 'source.ip': '1.2.3.4', - 'source.port': 40780, + 'destination.ip': ['1.2.3.4'], + 'destination.port': [5000], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['haproxy.log'], + 'event.module': ['haproxy'], + 'fileset.name': ['log'], + 'haproxy.frontend_name': ['main'], + 'haproxy.mode': ['HTTP'], + 'haproxy.source': ['1.2.3.4'], + 'input.type': ['log'], + 'log.offset': [0], + 'process.name': ['haproxy'], + 'process.pid': [24551], + 'service.type': ['haproxy'], + 'source.address': ['1.2.3.4'], + 'source.geo.continent_name': ['North America'], + 'source.geo.country_iso_code': ['US'], + 'source.geo.location.lat': [37.751], + 'source.geo.location.lon': [-97.822], + 'source.ip': ['1.2.3.4'], + 'source.port': [40780], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[HAProxy] ", - }, - Object { - "field": "source.address", - "highlights": Array [], - "value": "1.2.3.4", - }, - Object { - "constant": ":", - }, - Object { - "field": "source.port", - "highlights": Array [], - "value": "40780", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.frontend_name", - "highlights": Array [], - "value": "main", - }, -] -`); + Array [ + Object { + "constant": "[HAProxy] ", + }, + Object { + "field": "source.address", + "highlights": Array [], + "value": Array [ + "1.2.3.4", + ], + }, + Object { + "constant": ":", + }, + Object { + "field": "source.port", + "highlights": Array [], + "value": Array [ + 40780, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.frontend_name", + "highlights": Array [], + "value": Array [ + "main", + ], + }, + ] + `); }); test('haproxy tcp log', () => { const flattenedDocument = { - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'haproxy.log', - 'event.duration': 1000000, - 'event.module': 'haproxy', - 'fileset.name': 'log', - 'haproxy.backend_name': 'app', - 'haproxy.backend_queue': 0, - 'haproxy.bytes_read': 212, - 'haproxy.connection_wait_time_ms': -1, - 'haproxy.connections.active': 1, - 'haproxy.connections.backend': 0, - 'haproxy.connections.frontend': 1, - 'haproxy.connections.retries': 0, - 'haproxy.connections.server': 0, - 'haproxy.frontend_name': 'main', - 'haproxy.server_name': '', - 'haproxy.server_queue': 0, - 'haproxy.source': '127.0.0.1', - 'haproxy.termination_state': 'SC', - 'haproxy.total_waiting_time_ms': -1, - 'input.type': 'log', - 'log.offset': 0, - 'process.name': 'haproxy', - 'process.pid': 25457, - 'service.type': 'haproxy', - 'source.address': '127.0.0.1', - 'source.ip': '127.0.0.1', - 'source.port': 40962, + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['haproxy.log'], + 'event.duration': [1000000], + 'event.module': ['haproxy'], + 'fileset.name': ['log'], + 'haproxy.backend_name': ['app'], + 'haproxy.backend_queue': [0], + 'haproxy.bytes_read': [212], + 'haproxy.connection_wait_time_ms': [-1], + 'haproxy.connections.active': [1], + 'haproxy.connections.backend': [0], + 'haproxy.connections.frontend': [1], + 'haproxy.connections.retries': [0], + 'haproxy.connections.server': [0], + 'haproxy.frontend_name': ['main'], + 'haproxy.server_name': [''], + 'haproxy.server_queue': [0], + 'haproxy.source': ['127.0.0.1'], + 'haproxy.termination_state': ['SC'], + 'haproxy.total_waiting_time_ms': [-1], + 'input.type': ['log'], + 'log.offset': [0], + 'process.name': ['haproxy'], + 'process.pid': [25457], + 'service.type': ['haproxy'], + 'source.address': ['127.0.0.1'], + 'source.ip': ['127.0.0.1'], + 'source.port': [40962], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[HAProxy][tcp] ", - }, - Object { - "field": "source.address", - "highlights": Array [], - "value": "127.0.0.1", - }, - Object { - "constant": ":", - }, - Object { - "field": "source.port", - "highlights": Array [], - "value": "40962", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.frontend_name", - "highlights": Array [], - "value": "main", - }, - Object { - "constant": " -> ", - }, - Object { - "field": "haproxy.backend_name", - "highlights": Array [], - "value": "app", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.server_name", - "highlights": Array [], - "value": "", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.connections.active", - "highlights": Array [], - "value": "1", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.frontend", - "highlights": Array [], - "value": "1", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.backend", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.server", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.retries", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.server_queue", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.backend_queue", - "highlights": Array [], - "value": "0", - }, -] -`); + Array [ + Object { + "constant": "[HAProxy][tcp] ", + }, + Object { + "field": "source.address", + "highlights": Array [], + "value": Array [ + "127.0.0.1", + ], + }, + Object { + "constant": ":", + }, + Object { + "field": "source.port", + "highlights": Array [], + "value": Array [ + 40962, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.frontend_name", + "highlights": Array [], + "value": Array [ + "main", + ], + }, + Object { + "constant": " -> ", + }, + Object { + "field": "haproxy.backend_name", + "highlights": Array [], + "value": Array [ + "app", + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.server_name", + "highlights": Array [], + "value": Array [ + "", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.connections.active", + "highlights": Array [], + "value": Array [ + 1, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.frontend", + "highlights": Array [], + "value": Array [ + 1, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.backend", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.server", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.retries", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.server_queue", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.backend_queue", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + ] + `); }); test('haproxy http log', () => { const flattenedDocument = { - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'haproxy.log', - 'event.duration': 2000000, - 'event.module': 'haproxy', - 'fileset.name': 'log', - 'haproxy.backend_name': 'docs_microservice', - 'haproxy.backend_queue': 0, - 'haproxy.bytes_read': 168, - 'haproxy.connection_wait_time_ms': 1, - 'haproxy.connections.active': 6, - 'haproxy.connections.backend': 0, - 'haproxy.connections.frontend': 6, - 'haproxy.connections.retries': 0, - 'haproxy.connections.server': 0, - 'haproxy.frontend_name': 'incoming~', - 'haproxy.http.request.captured_cookie': '-', + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['haproxy.log'], + 'event.duration': [2000000], + 'event.module': ['haproxy'], + 'fileset.name': ['log'], + 'haproxy.backend_name': ['docs_microservice'], + 'haproxy.backend_queue': [0], + 'haproxy.bytes_read': [168], + 'haproxy.connection_wait_time_ms': [1], + 'haproxy.connections.active': [6], + 'haproxy.connections.backend': [0], + 'haproxy.connections.frontend': [6], + 'haproxy.connections.retries': [0], + 'haproxy.connections.server': [0], + 'haproxy.frontend_name': ['incoming~'], + 'haproxy.http.request.captured_cookie': ['-'], 'haproxy.http.request.captured_headers': ['docs.example.internal'], - 'haproxy.http.request.raw_request_line': + 'haproxy.http.request.raw_request_line': [ 'GET /component---src-pages-index-js-4b15624544f97cf0bb8f.js HTTP/1.1', - 'haproxy.http.request.time_wait_ms': 0, - 'haproxy.http.request.time_wait_without_data_ms': 0, - 'haproxy.http.response.captured_cookie': '-', + ], + 'haproxy.http.request.time_wait_ms': [0], + 'haproxy.http.request.time_wait_without_data_ms': [0], + 'haproxy.http.response.captured_cookie': ['-'], 'haproxy.http.response.captured_headers': [], - 'haproxy.server_name': 'docs', - 'haproxy.server_queue': 0, - 'haproxy.termination_state': '----', - 'haproxy.total_waiting_time_ms': 0, - 'http.response.bytes': 168, - 'http.response.status_code': 304, - 'input.type': 'log', - 'log.offset': 0, - 'process.name': 'haproxy', - 'process.pid': 32450, - 'service.type': 'haproxy', - 'source.address': '1.2.3.4', - 'source.geo.continent_name': 'North America', - 'source.geo.country_iso_code': 'US', - 'source.geo.location.lat': 37.751, - 'source.geo.location.lon': -97.822, - 'source.ip': '1.2.3.4', - 'source.port': 38862, + 'haproxy.server_name': ['docs'], + 'haproxy.server_queue': [0], + 'haproxy.termination_state': ['----'], + 'haproxy.total_waiting_time_ms': [0], + 'http.response.bytes': [168], + 'http.response.status_code': [304], + 'input.type': ['log'], + 'log.offset': [0], + 'process.name': ['haproxy'], + 'process.pid': [32450], + 'service.type': ['haproxy'], + 'source.address': ['1.2.3.4'], + 'source.geo.continent_name': ['North America'], + 'source.geo.country_iso_code': ['US'], + 'source.geo.location.lat': [37.751], + 'source.geo.location.lon': [-97.822], + 'source.ip': ['1.2.3.4'], + 'source.port': [38862], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[HAProxy][http] ", - }, - Object { - "field": "source.address", - "highlights": Array [], - "value": "1.2.3.4", - }, - Object { - "constant": ":", - }, - Object { - "field": "source.port", - "highlights": Array [], - "value": "38862", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.frontend_name", - "highlights": Array [], - "value": "incoming~", - }, - Object { - "constant": " -> ", - }, - Object { - "field": "haproxy.backend_name", - "highlights": Array [], - "value": "docs_microservice", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.server_name", - "highlights": Array [], - "value": "docs", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "haproxy.http.request.raw_request_line", - "highlights": Array [], - "value": "GET /component---src-pages-index-js-4b15624544f97cf0bb8f.js HTTP/1.1", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "http.response.status_code", - "highlights": Array [], - "value": "304", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.http.request.time_wait_ms", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "event.duration", - "highlights": Array [], - "value": "2000000", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connection_wait_time_ms", - "highlights": Array [], - "value": "1", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.http.request.time_wait_without_data_ms", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "event.duration", - "highlights": Array [], - "value": "2000000", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.connections.active", - "highlights": Array [], - "value": "6", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.frontend", - "highlights": Array [], - "value": "6", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.backend", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.server", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.retries", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.server_queue", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.backend_queue", - "highlights": Array [], - "value": "0", - }, -] -`); + Array [ + Object { + "constant": "[HAProxy][http] ", + }, + Object { + "field": "source.address", + "highlights": Array [], + "value": Array [ + "1.2.3.4", + ], + }, + Object { + "constant": ":", + }, + Object { + "field": "source.port", + "highlights": Array [], + "value": Array [ + 38862, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.frontend_name", + "highlights": Array [], + "value": Array [ + "incoming~", + ], + }, + Object { + "constant": " -> ", + }, + Object { + "field": "haproxy.backend_name", + "highlights": Array [], + "value": Array [ + "docs_microservice", + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.server_name", + "highlights": Array [], + "value": Array [ + "docs", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "haproxy.http.request.raw_request_line", + "highlights": Array [], + "value": Array [ + "GET /component---src-pages-index-js-4b15624544f97cf0bb8f.js HTTP/1.1", + ], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "http.response.status_code", + "highlights": Array [], + "value": Array [ + 304, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.http.request.time_wait_ms", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "event.duration", + "highlights": Array [], + "value": Array [ + 2000000, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connection_wait_time_ms", + "highlights": Array [], + "value": Array [ + 1, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.http.request.time_wait_without_data_ms", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "event.duration", + "highlights": Array [], + "value": Array [ + 2000000, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.connections.active", + "highlights": Array [], + "value": Array [ + 6, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.frontend", + "highlights": Array [], + "value": Array [ + 6, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.backend", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.server", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.retries", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.server_queue", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.backend_queue", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + ] + `); }); }); describe('in pre-ECS format', () => { test('haproxy default log', () => { const flattenedDocument = { - 'event.dataset': 'haproxy.log', - 'fileset.module': 'haproxy', - 'fileset.name': 'log', - 'haproxy.client.ip': '1.2.3.4', - 'haproxy.client.port': '40780', - 'haproxy.destination.ip': '1.2.3.4', - 'haproxy.destination.port': '5000', - 'haproxy.frontend_name': 'main', - 'haproxy.geoip.continent_name': 'North America', - 'haproxy.geoip.country_iso_code': 'US', - 'haproxy.geoip.location.lat': 37.751, - 'haproxy.geoip.location.lon': -97.822, - 'haproxy.mode': 'HTTP', - 'haproxy.pid': '24551', - 'haproxy.process_name': 'haproxy', - 'haproxy.source': '1.2.3.4', - 'input.type': 'log', - offset: 0, - 'prospector.type': 'log', + 'event.dataset': ['haproxy.log'], + 'fileset.module': ['haproxy'], + 'fileset.name': ['log'], + 'haproxy.client.ip': ['1.2.3.4'], + 'haproxy.client.port': ['40780'], + 'haproxy.destination.ip': ['1.2.3.4'], + 'haproxy.destination.port': ['5000'], + 'haproxy.frontend_name': ['main'], + 'haproxy.geoip.continent_name': ['North America'], + 'haproxy.geoip.country_iso_code': ['US'], + 'haproxy.geoip.location.lat': [37.751], + 'haproxy.geoip.location.lon': [-97.822], + 'haproxy.mode': ['HTTP'], + 'haproxy.pid': ['24551'], + 'haproxy.process_name': ['haproxy'], + 'haproxy.source': ['1.2.3.4'], + 'input.type': ['log'], + offset: [0], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[HAProxy] ", - }, - Object { - "field": "haproxy.client.ip", - "highlights": Array [], - "value": "1.2.3.4", - }, - Object { - "constant": ":", - }, - Object { - "field": "haproxy.client.port", - "highlights": Array [], - "value": "40780", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.frontend_name", - "highlights": Array [], - "value": "main", - }, -] -`); + Array [ + Object { + "constant": "[HAProxy] ", + }, + Object { + "field": "haproxy.client.ip", + "highlights": Array [], + "value": Array [ + "1.2.3.4", + ], + }, + Object { + "constant": ":", + }, + Object { + "field": "haproxy.client.port", + "highlights": Array [], + "value": Array [ + "40780", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.frontend_name", + "highlights": Array [], + "value": Array [ + "main", + ], + }, + ] + `); }); test('haproxy tcp log', () => { const flattenedDocument = { - 'event.dataset': 'haproxy.log', - 'fileset.module': 'haproxy', - 'fileset.name': 'log', - 'haproxy.backend_name': 'app', - 'haproxy.backend_queue': 0, - 'haproxy.bytes_read': 212, - 'haproxy.client.ip': '127.0.0.1', - 'haproxy.client.port': 40962, - 'haproxy.connection_wait_time_ms': -1, - 'haproxy.connections.active': 1, - 'haproxy.connections.backend': 0, - 'haproxy.connections.frontend': 1, - 'haproxy.connections.retries': 0, - 'haproxy.connections.server': 0, - 'haproxy.frontend_name': 'main', - 'haproxy.pid': 25457, - 'haproxy.process_name': 'haproxy', - 'haproxy.server_name': '', - 'haproxy.server_queue': 0, - 'haproxy.source': '127.0.0.1', - 'haproxy.tcp.processing_time_ms': 0, - 'haproxy.termination_state': 'SC', - 'haproxy.total_waiting_time_ms': -1, - 'input.type': 'log', - offset: 0, - 'prospector.type': 'log', + 'event.dataset': ['haproxy.log'], + 'fileset.module': ['haproxy'], + 'fileset.name': ['log'], + 'haproxy.backend_name': ['app'], + 'haproxy.backend_queue': [0], + 'haproxy.bytes_read': [212], + 'haproxy.client.ip': ['127.0.0.1'], + 'haproxy.client.port': [40962], + 'haproxy.connection_wait_time_ms': [-1], + 'haproxy.connections.active': [1], + 'haproxy.connections.backend': [0], + 'haproxy.connections.frontend': [1], + 'haproxy.connections.retries': [0], + 'haproxy.connections.server': [0], + 'haproxy.frontend_name': ['main'], + 'haproxy.pid': [25457], + 'haproxy.process_name': ['haproxy'], + 'haproxy.server_name': [''], + 'haproxy.server_queue': [0], + 'haproxy.source': ['127.0.0.1'], + 'haproxy.tcp.processing_time_ms': [0], + 'haproxy.termination_state': ['SC'], + 'haproxy.total_waiting_time_ms': [-1], + 'input.type': ['log'], + offset: [0], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[HAProxy][tcp] ", - }, - Object { - "field": "haproxy.client.ip", - "highlights": Array [], - "value": "127.0.0.1", - }, - Object { - "constant": ":", - }, - Object { - "field": "haproxy.client.port", - "highlights": Array [], - "value": "40962", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.frontend_name", - "highlights": Array [], - "value": "main", - }, - Object { - "constant": " -> ", - }, - Object { - "field": "haproxy.backend_name", - "highlights": Array [], - "value": "app", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.server_name", - "highlights": Array [], - "value": "", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.connections.active", - "highlights": Array [], - "value": "1", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.frontend", - "highlights": Array [], - "value": "1", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.backend", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.server", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.retries", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.server_queue", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.backend_queue", - "highlights": Array [], - "value": "0", - }, -] -`); + Array [ + Object { + "constant": "[HAProxy][tcp] ", + }, + Object { + "field": "haproxy.client.ip", + "highlights": Array [], + "value": Array [ + "127.0.0.1", + ], + }, + Object { + "constant": ":", + }, + Object { + "field": "haproxy.client.port", + "highlights": Array [], + "value": Array [ + 40962, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.frontend_name", + "highlights": Array [], + "value": Array [ + "main", + ], + }, + Object { + "constant": " -> ", + }, + Object { + "field": "haproxy.backend_name", + "highlights": Array [], + "value": Array [ + "app", + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.server_name", + "highlights": Array [], + "value": Array [ + "", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.connections.active", + "highlights": Array [], + "value": Array [ + 1, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.frontend", + "highlights": Array [], + "value": Array [ + 1, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.backend", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.server", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.retries", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.server_queue", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.backend_queue", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + ] + `); }); test('haproxy http log', () => { const flattenedDocument = { - 'event.dataset': 'haproxy.log', - 'fileset.module': 'haproxy', - 'fileset.name': 'log', - 'haproxy.backend_name': 'docs_microservice', - 'haproxy.backend_queue': 0, - 'haproxy.bytes_read': 168, - 'haproxy.client.ip': '1.2.3.4', - 'haproxy.client.port': 38862, - 'haproxy.connection_wait_time_ms': 1, - 'haproxy.connections.active': 6, - 'haproxy.connections.backend': 0, - 'haproxy.connections.frontend': 6, - 'haproxy.connections.retries': 0, - 'haproxy.connections.server': 0, - 'haproxy.frontend_name': 'incoming~', - 'haproxy.geoip.continent_name': 'North America', - 'haproxy.geoip.country_iso_code': 'US', - 'haproxy.geoip.location.lat': 37.751, - 'haproxy.geoip.location.lon': -97.822, - 'haproxy.http.request.captured_cookie': '-', - 'haproxy.http.request.raw_request_line': + 'event.dataset': ['haproxy.log'], + 'fileset.module': ['haproxy'], + 'fileset.name': ['log'], + 'haproxy.backend_name': ['docs_microservice'], + 'haproxy.backend_queue': [0], + 'haproxy.bytes_read': [168], + 'haproxy.client.ip': ['1.2.3.4'], + 'haproxy.client.port': [38862], + 'haproxy.connection_wait_time_ms': [1], + 'haproxy.connections.active': [6], + 'haproxy.connections.backend': [0], + 'haproxy.connections.frontend': [6], + 'haproxy.connections.retries': [0], + 'haproxy.connections.server': [0], + 'haproxy.frontend_name': ['incoming~'], + 'haproxy.geoip.continent_name': ['North America'], + 'haproxy.geoip.country_iso_code': ['US'], + 'haproxy.geoip.location.lat': [37.751], + 'haproxy.geoip.location.lon': [-97.822], + 'haproxy.http.request.captured_cookie': ['-'], + 'haproxy.http.request.raw_request_line': [ 'GET /component---src-pages-index-js-4b15624544f97cf0bb8f.js HTTP/1.1', - 'haproxy.http.request.time_active_ms': 2, - 'haproxy.http.request.time_wait_ms': 0, - 'haproxy.http.request.time_wait_without_data_ms': 0, - 'haproxy.http.response.captured_cookie': '-', - 'haproxy.http.response.status_code': 304, - 'haproxy.pid': 32450, - 'haproxy.process_name': 'haproxy', - 'haproxy.server_name': 'docs', - 'haproxy.server_queue': 0, - 'haproxy.termination_state': '----', - 'haproxy.total_waiting_time_ms': 0, - 'input.type': 'log', - offset: 0, - 'prospector.type': 'log', + ], + 'haproxy.http.request.time_active_ms': [2], + 'haproxy.http.request.time_wait_ms': [0], + 'haproxy.http.request.time_wait_without_data_ms': [0], + 'haproxy.http.response.captured_cookie': ['-'], + 'haproxy.http.response.status_code': [304], + 'haproxy.pid': [32450], + 'haproxy.process_name': ['haproxy'], + 'haproxy.server_name': ['docs'], + 'haproxy.server_queue': [0], + 'haproxy.termination_state': ['----'], + 'haproxy.total_waiting_time_ms': [0], + 'input.type': ['log'], + offset: [0], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[HAProxy][http] ", - }, - Object { - "field": "haproxy.client.ip", - "highlights": Array [], - "value": "1.2.3.4", - }, - Object { - "constant": ":", - }, - Object { - "field": "haproxy.client.port", - "highlights": Array [], - "value": "38862", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.frontend_name", - "highlights": Array [], - "value": "incoming~", - }, - Object { - "constant": " -> ", - }, - Object { - "field": "haproxy.backend_name", - "highlights": Array [], - "value": "docs_microservice", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.server_name", - "highlights": Array [], - "value": "docs", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "haproxy.http.request.raw_request_line", - "highlights": Array [], - "value": "GET /component---src-pages-index-js-4b15624544f97cf0bb8f.js HTTP/1.1", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "haproxy.http.response.status_code", - "highlights": Array [], - "value": "304", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.http.request.time_wait_ms", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.total_waiting_time_ms", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connection_wait_time_ms", - "highlights": Array [], - "value": "1", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.http.request.time_wait_without_data_ms", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.http.request.time_active_ms", - "highlights": Array [], - "value": "2", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.connections.active", - "highlights": Array [], - "value": "6", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.frontend", - "highlights": Array [], - "value": "6", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.backend", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.server", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.connections.retries", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": " ", - }, - Object { - "field": "haproxy.server_queue", - "highlights": Array [], - "value": "0", - }, - Object { - "constant": "/", - }, - Object { - "field": "haproxy.backend_queue", - "highlights": Array [], - "value": "0", - }, -] -`); + Array [ + Object { + "constant": "[HAProxy][http] ", + }, + Object { + "field": "haproxy.client.ip", + "highlights": Array [], + "value": Array [ + "1.2.3.4", + ], + }, + Object { + "constant": ":", + }, + Object { + "field": "haproxy.client.port", + "highlights": Array [], + "value": Array [ + 38862, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.frontend_name", + "highlights": Array [], + "value": Array [ + "incoming~", + ], + }, + Object { + "constant": " -> ", + }, + Object { + "field": "haproxy.backend_name", + "highlights": Array [], + "value": Array [ + "docs_microservice", + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.server_name", + "highlights": Array [], + "value": Array [ + "docs", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "haproxy.http.request.raw_request_line", + "highlights": Array [], + "value": Array [ + "GET /component---src-pages-index-js-4b15624544f97cf0bb8f.js HTTP/1.1", + ], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "haproxy.http.response.status_code", + "highlights": Array [], + "value": Array [ + 304, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.http.request.time_wait_ms", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.total_waiting_time_ms", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connection_wait_time_ms", + "highlights": Array [], + "value": Array [ + 1, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.http.request.time_wait_without_data_ms", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.http.request.time_active_ms", + "highlights": Array [], + "value": Array [ + 2, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.connections.active", + "highlights": Array [], + "value": Array [ + 6, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.frontend", + "highlights": Array [], + "value": Array [ + 6, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.backend", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.server", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.connections.retries", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "haproxy.server_queue", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + Object { + "constant": "/", + }, + Object { + "field": "haproxy.backend_queue", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_icinga.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_icinga.test.ts index 120137f15b883..00d282e1833d8 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_icinga.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_icinga.test.ts @@ -13,135 +13,155 @@ describe('Filebeat Rules', () => { describe('in pre-ECS format', () => { test('icinga debug log', () => { const flattenedDocument = { - '@timestamp': '2017-04-04T11:43:09.000Z', - 'event.dataset': 'icinga.debug', - 'fileset.module': 'icinga', - 'fileset.name': 'debug', - 'icinga.debug.facility': 'GraphiteWriter', - 'icinga.debug.message': + '@timestamp': ['2017-04-04T11:43:09.000Z'], + 'event.dataset': ['icinga.debug'], + 'fileset.module': ['icinga'], + 'fileset.name': ['debug'], + 'icinga.debug.facility': ['GraphiteWriter'], + 'icinga.debug.message': [ "Add to metric list:'icinga2.demo.services.procs.procs.perfdata.procs.warn 250 1491306189'.", - 'icinga.debug.severity': 'debug', - 'input.type': 'log', - offset: 0, - 'prospector.type': 'log', + ], + 'icinga.debug.severity': ['debug'], + 'input.type': ['log'], + offset: [0], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[Icinga][", - }, - Object { - "field": "icinga.debug.facility", - "highlights": Array [], - "value": "GraphiteWriter", - }, - Object { - "constant": "][", - }, - Object { - "field": "icinga.debug.severity", - "highlights": Array [], - "value": "debug", - }, - Object { - "constant": "] ", - }, - Object { - "field": "icinga.debug.message", - "highlights": Array [], - "value": "Add to metric list:'icinga2.demo.services.procs.procs.perfdata.procs.warn 250 1491306189'.", - }, -] -`); + Array [ + Object { + "constant": "[Icinga][", + }, + Object { + "field": "icinga.debug.facility", + "highlights": Array [], + "value": Array [ + "GraphiteWriter", + ], + }, + Object { + "constant": "][", + }, + Object { + "field": "icinga.debug.severity", + "highlights": Array [], + "value": Array [ + "debug", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "icinga.debug.message", + "highlights": Array [], + "value": Array [ + "Add to metric list:'icinga2.demo.services.procs.procs.perfdata.procs.warn 250 1491306189'.", + ], + }, + ] + `); }); test('icinga main log', () => { const flattenedDocument = { - '@timestamp': '2017-04-04T09:16:34.000Z', - 'event.dataset': 'icinga.main', - 'fileset.module': 'icinga', - 'fileset.name': 'main', - 'icinga.main.facility': 'Notification', - 'icinga.main.message': + '@timestamp': ['2017-04-04T09:16:34.000Z'], + 'event.dataset': ['icinga.main'], + 'fileset.module': ['icinga'], + 'fileset.name': ['main'], + 'icinga.main.facility': ['Notification'], + 'icinga.main.message': [ "Sending 'Recovery' notification 'demo!load!mail-icingaadmin for user 'on-call'", - 'icinga.main.severity': 'information', - 'input.type': 'log', - offset: 0, - 'prospector.type': 'log', + ], + 'icinga.main.severity': ['information'], + 'input.type': ['log'], + offset: [0], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[Icinga][", - }, - Object { - "field": "icinga.main.facility", - "highlights": Array [], - "value": "Notification", - }, - Object { - "constant": "][", - }, - Object { - "field": "icinga.main.severity", - "highlights": Array [], - "value": "information", - }, - Object { - "constant": "] ", - }, - Object { - "field": "icinga.main.message", - "highlights": Array [], - "value": "Sending 'Recovery' notification 'demo!load!mail-icingaadmin for user 'on-call'", - }, -] -`); + Array [ + Object { + "constant": "[Icinga][", + }, + Object { + "field": "icinga.main.facility", + "highlights": Array [], + "value": Array [ + "Notification", + ], + }, + Object { + "constant": "][", + }, + Object { + "field": "icinga.main.severity", + "highlights": Array [], + "value": Array [ + "information", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "icinga.main.message", + "highlights": Array [], + "value": Array [ + "Sending 'Recovery' notification 'demo!load!mail-icingaadmin for user 'on-call'", + ], + }, + ] + `); }); test('icinga startup log', () => { const flattenedDocument = { - 'event.dataset': 'icinga.startup', - 'fileset.module': 'icinga', - 'fileset.name': 'startup', - 'icinga.startup.facility': 'cli', - 'icinga.startup.message': 'Icinga application loader (version: r2.6.3-1)', - 'icinga.startup.severity': 'information', - 'input.type': 'log', - offset: 0, - 'prospector.type': 'log', + 'event.dataset': ['icinga.startup'], + 'fileset.module': ['icinga'], + 'fileset.name': ['startup'], + 'icinga.startup.facility': ['cli'], + 'icinga.startup.message': ['Icinga application loader (version: r2.6.3-1)'], + 'icinga.startup.severity': ['information'], + 'input.type': ['log'], + offset: [0], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[Icinga][", - }, - Object { - "field": "icinga.startup.facility", - "highlights": Array [], - "value": "cli", - }, - Object { - "constant": "][", - }, - Object { - "field": "icinga.startup.severity", - "highlights": Array [], - "value": "information", - }, - Object { - "constant": "] ", - }, - Object { - "field": "icinga.startup.message", - "highlights": Array [], - "value": "Icinga application loader (version: r2.6.3-1)", - }, -] -`); + Array [ + Object { + "constant": "[Icinga][", + }, + Object { + "field": "icinga.startup.facility", + "highlights": Array [], + "value": Array [ + "cli", + ], + }, + Object { + "constant": "][", + }, + Object { + "field": "icinga.startup.severity", + "highlights": Array [], + "value": Array [ + "information", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "icinga.startup.message", + "highlights": Array [], + "value": Array [ + "Icinga application loader (version: r2.6.3-1)", + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_iis.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_iis.test.ts index 72449c03b63a6..5238d5fe1da95 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_iis.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_iis.test.ts @@ -13,550 +13,622 @@ describe('Filebeat Rules', () => { describe('in ECS format', () => { test('iis access log', () => { const flattenedDocument = { - '@timestamp': '2018-01-01T10:11:12.000Z', - 'destination.address': '127.0.0.1', - 'destination.domain': 'example.com', - 'destination.ip': '127.0.0.1', - 'destination.port': 80, - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'iis.access', - 'event.duration': 789000000, - 'event.module': 'iis', - 'fileset.name': 'access', - 'http.request.body.bytes': 456, - 'http.request.method': 'GET', - 'http.request.referrer': '-', - 'http.response.body.bytes': 123, - 'http.response.status_code': 200, - 'http.version': '1.1', - 'iis.access.cookie': '-', - 'iis.access.server_name': 'MACHINE-NAME', - 'iis.access.site_name': 'W3SVC1', - 'iis.access.sub_status': 0, - 'iis.access.win32_status': 0, - 'input.type': 'log', - 'log.offset': 1204, - 'service.type': 'iis', - 'source.address': '85.181.35.98', - 'source.geo.city_name': 'Berlin', - 'source.geo.continent_name': 'Europe', - 'source.geo.country_iso_code': 'DE', - 'source.geo.location.lat': 52.4908, - 'source.geo.location.lon': 13.3275, - 'source.geo.region_iso_code': 'DE-BE', - 'source.geo.region_name': 'Land Berlin', - 'source.ip': '85.181.35.98', - 'url.path': '/', - 'url.query': 'q=100', - 'user.name': '-', - 'user_agent.device.name': 'Other', - 'user_agent.name': 'Chrome', - 'user_agent.original': + '@timestamp': ['2018-01-01T10:11:12.000Z'], + 'destination.address': ['127.0.0.1'], + 'destination.domain': ['example.com'], + 'destination.ip': ['127.0.0.1'], + 'destination.port': [80], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['iis.access'], + 'event.duration': [789000000], + 'event.module': ['iis'], + 'fileset.name': ['access'], + 'http.request.body.bytes': [456], + 'http.request.method': ['GET'], + 'http.request.referrer': ['-'], + 'http.response.body.bytes': [123], + 'http.response.status_code': [200], + 'http.version': ['1.1'], + 'iis.access.cookie': ['-'], + 'iis.access.server_name': ['MACHINE-NAME'], + 'iis.access.site_name': ['W3SVC1'], + 'iis.access.sub_status': [0], + 'iis.access.win32_status': [0], + 'input.type': ['log'], + 'log.offset': [1204], + 'service.type': ['iis'], + 'source.address': ['85.181.35.98'], + 'source.geo.city_name': ['Berlin'], + 'source.geo.continent_name': ['Europe'], + 'source.geo.country_iso_code': ['DE'], + 'source.geo.location.lat': [52.4908], + 'source.geo.location.lon': [13.3275], + 'source.geo.region_iso_code': ['DE-BE'], + 'source.geo.region_name': ['Land Berlin'], + 'source.ip': ['85.181.35.98'], + 'url.path': ['/'], + 'url.query': ['q=100'], + 'user.name': ['-'], + 'user_agent.device.name': ['Other'], + 'user_agent.name': ['Chrome'], + 'user_agent.original': [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', - 'user_agent.os.full': 'Mac OS X 10.14.0', - 'user_agent.os.name': 'Mac OS X', - 'user_agent.os.version': '10.14.0', - 'user_agent.version': '70.0.3538', + ], + 'user_agent.os.full': ['Mac OS X 10.14.0'], + 'user_agent.os.name': ['Mac OS X'], + 'user_agent.os.version': ['10.14.0'], + 'user_agent.version': ['70.0.3538'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "event.module", - "highlights": Array [], - "value": "iis", - }, - Object { - "constant": "][access] ", - }, - Object { - "field": "source.ip", - "highlights": Array [], - "value": "85.181.35.98", - }, - Object { - "constant": " ", - }, - Object { - "field": "user.name", - "highlights": Array [], - "value": "-", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "http.request.method", - "highlights": Array [], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "url.path", - "highlights": Array [], - "value": "/", - }, - Object { - "constant": "?", - }, - Object { - "field": "url.query", - "highlights": Array [], - "value": "q=100", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "http.version", - "highlights": Array [], - "value": "1.1", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "http.response.status_code", - "highlights": Array [], - "value": "200", - }, - Object { - "constant": " ", - }, - Object { - "field": "http.response.body.bytes", - "highlights": Array [], - "value": "123", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "event.module", + "highlights": Array [], + "value": Array [ + "iis", + ], + }, + Object { + "constant": "][access] ", + }, + Object { + "field": "source.ip", + "highlights": Array [], + "value": Array [ + "85.181.35.98", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "user.name", + "highlights": Array [], + "value": Array [ + "-", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "http.request.method", + "highlights": Array [], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "url.path", + "highlights": Array [], + "value": Array [ + "/", + ], + }, + Object { + "constant": "?", + }, + Object { + "field": "url.query", + "highlights": Array [], + "value": Array [ + "q=100", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "http.version", + "highlights": Array [], + "value": Array [ + "1.1", + ], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "http.response.status_code", + "highlights": Array [], + "value": Array [ + 200, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "http.response.body.bytes", + "highlights": Array [], + "value": Array [ + 123, + ], + }, + ] + `); }); test('iis 7.5 access log', () => { const flattenedDocument = { - '@timestamp': '2018-08-28T18:24:25.000Z', - 'destination.address': '10.100.220.70', - 'destination.ip': '10.100.220.70', - 'destination.port': 80, - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'iis.access', - 'event.duration': 792000000, - 'event.module': 'iis', - 'fileset.name': 'access', - 'http.request.method': 'GET', - 'http.response.status_code': 404, - 'iis.access.sub_status': 4, - 'iis.access.win32_status': 2, - 'input.type': 'log', - 'log.offset': 244, - 'service.type': 'iis', - 'source.address': '10.100.118.31', - 'source.ip': '10.100.118.31', - 'url.path': '/', - 'url.query': 'q=100', - 'user.name': '-', - 'user_agent.device.name': 'Other', - 'user_agent.name': 'IE', - 'user_agent.original': - 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; WOW64; Trident/7.0; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR[ 2.0.50727](tel: 2050727); .NET CLR 3.0.30729)', - 'user_agent.os.name': 'Windows 8.1', - 'user_agent.version': '7.0', + '@timestamp': ['2018-08-28T18:24:25.000Z'], + 'destination.address': ['10.100.220.70'], + 'destination.ip': ['10.100.220.70'], + 'destination.port': [80], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['iis.access'], + 'event.duration': [792000000], + 'event.module': ['iis'], + 'fileset.name': ['access'], + 'http.request.method': ['GET'], + 'http.response.status_code': [404], + 'iis.access.sub_status': [4], + 'iis.access.win32_status': [2], + 'input.type': ['log'], + 'log.offset': [244], + 'service.type': ['iis'], + 'source.address': ['10.100.118.31'], + 'source.ip': ['10.100.118.31'], + 'url.path': ['/'], + 'url.query': ['q=100'], + 'user.name': ['-'], + 'user_agent.device.name': ['Other'], + 'user_agent.name': ['IE'], + 'user_agent.original': [ + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; WOW64; Trident/7.0; .NET4.0E; .NET4.0C; .NET CLR 3.5.30729; .NET CLR[ 2.0.50727](tel: [2050727); .NET CLR 3.0.30729)', + ], + 'user_agent.os.name': ['Windows 8.1'], + 'user_agent.version': ['7.0'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "event.module", - "highlights": Array [], - "value": "iis", - }, - Object { - "constant": "][access] ", - }, - Object { - "field": "source.ip", - "highlights": Array [], - "value": "10.100.118.31", - }, - Object { - "constant": " ", - }, - Object { - "field": "user.name", - "highlights": Array [], - "value": "-", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "http.request.method", - "highlights": Array [], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "url.path", - "highlights": Array [], - "value": "/", - }, - Object { - "constant": "?", - }, - Object { - "field": "url.query", - "highlights": Array [], - "value": "q=100", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "http.version", - "highlights": Array [], - "value": "undefined", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "http.response.status_code", - "highlights": Array [], - "value": "404", - }, - Object { - "constant": " ", - }, - Object { - "field": "http.response.body.bytes", - "highlights": Array [], - "value": "undefined", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "event.module", + "highlights": Array [], + "value": Array [ + "iis", + ], + }, + Object { + "constant": "][access] ", + }, + Object { + "field": "source.ip", + "highlights": Array [], + "value": Array [ + "10.100.118.31", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "user.name", + "highlights": Array [], + "value": Array [ + "-", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "http.request.method", + "highlights": Array [], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "url.path", + "highlights": Array [], + "value": Array [ + "/", + ], + }, + Object { + "constant": "?", + }, + Object { + "field": "url.query", + "highlights": Array [], + "value": Array [ + "q=100", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "http.version", + "highlights": Array [], + "value": Array [], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "http.response.status_code", + "highlights": Array [], + "value": Array [ + 404, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "http.response.body.bytes", + "highlights": Array [], + "value": Array [], + }, + ] + `); }); test('iis error log', () => { const flattenedDocument = { - '@timestamp': '2018-01-01T08:09:10.000Z', - 'destination.address': '172.31.77.6', - 'destination.ip': '172.31.77.6', - 'destination.port': 80, - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'iis.error', - 'event.module': 'iis', - 'fileset.name': 'error', - 'http.request.method': 'GET', - 'http.response.status_code': 503, - 'http.version': '1.1', - 'iis.error.queue_name': '-', - 'iis.error.reason_phrase': 'ConnLimit', - 'input.type': 'log', - 'log.offset': 186, - 'service.type': 'iis', - 'source.address': '172.31.77.6', - 'source.ip': '172.31.77.6', - 'source.port': 2094, - 'url.original': '/qos/1kbfile.txt', + '@timestamp': ['2018-01-01T08:09:10.000Z'], + 'destination.address': ['172.31.77.6'], + 'destination.ip': ['172.31.77.6'], + 'destination.port': [80], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['iis.error'], + 'event.module': ['iis'], + 'fileset.name': ['error'], + 'http.request.method': ['GET'], + 'http.response.status_code': [503], + 'http.version': ['1.1'], + 'iis.error.queue_name': ['-'], + 'iis.error.reason_phrase': ['ConnLimit'], + 'input.type': ['log'], + 'log.offset': [186], + 'service.type': ['iis'], + 'source.address': ['172.31.77.6'], + 'source.ip': ['172.31.77.6'], + 'source.port': [2094], + 'url.original': ['/qos/1kbfile.txt'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[iis][error] ", - }, - Object { - "field": "source.ip", - "highlights": Array [], - "value": "172.31.77.6", - }, - Object { - "constant": " ", - }, - Object { - "field": "iis.error.reason_phrase", - "highlights": Array [], - "value": "ConnLimit", - }, -] -`); + Array [ + Object { + "constant": "[iis][error] ", + }, + Object { + "field": "source.ip", + "highlights": Array [], + "value": Array [ + "172.31.77.6", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "iis.error.reason_phrase", + "highlights": Array [], + "value": Array [ + "ConnLimit", + ], + }, + ] + `); }); }); describe('in pre-ECS format', () => { test('iis access log', () => { const flattenedDocument = { - '@timestamp': '2018-01-01T08:09:10.000Z', - 'event.dataset': 'iis.access', - 'fileset.module': 'iis', - 'fileset.name': 'access', - 'iis.access.geoip.city_name': 'Berlin', - 'iis.access.geoip.continent_name': 'Europe', - 'iis.access.geoip.country_iso_code': 'DE', - 'iis.access.geoip.location.lat': 52.4908, - 'iis.access.geoip.location.lon': 13.3275, - 'iis.access.geoip.region_iso_code': 'DE-BE', - 'iis.access.geoip.region_name': 'Land Berlin', - 'iis.access.method': 'GET', - 'iis.access.port': '80', - 'iis.access.query_string': 'q=100', - 'iis.access.referrer': '-', - 'iis.access.remote_ip': '85.181.35.98', - 'iis.access.request_time_ms': '123', - 'iis.access.response_code': '200', - 'iis.access.server_ip': '127.0.0.1', - 'iis.access.sub_status': '0', - 'iis.access.url': '/', - 'iis.access.user_agent.device': 'Other', - 'iis.access.user_agent.major': '57', - 'iis.access.user_agent.minor': '0', - 'iis.access.user_agent.name': 'Firefox', - 'iis.access.user_agent.original': + '@timestamp': ['2018-01-01T08:09:10.000Z'], + 'event.dataset': ['iis.access'], + 'fileset.module': ['iis'], + 'fileset.name': ['access'], + 'iis.access.geoip.city_name': ['Berlin'], + 'iis.access.geoip.continent_name': ['Europe'], + 'iis.access.geoip.country_iso_code': ['DE'], + 'iis.access.geoip.location.lat': [52.4908], + 'iis.access.geoip.location.lon': [13.3275], + 'iis.access.geoip.region_iso_code': ['DE-BE'], + 'iis.access.geoip.region_name': ['Land Berlin'], + 'iis.access.method': ['GET'], + 'iis.access.port': ['80'], + 'iis.access.query_string': ['q=100'], + 'iis.access.referrer': ['-'], + 'iis.access.remote_ip': ['85.181.35.98'], + 'iis.access.request_time_ms': ['123'], + 'iis.access.response_code': ['200'], + 'iis.access.server_ip': ['127.0.0.1'], + 'iis.access.sub_status': ['0'], + 'iis.access.url': ['/'], + 'iis.access.user_agent.device': ['Other'], + 'iis.access.user_agent.major': ['57'], + 'iis.access.user_agent.minor': ['0'], + 'iis.access.user_agent.name': ['Firefox'], + 'iis.access.user_agent.original': [ 'Mozilla/5.0+(Windows+NT+6.1;+Win64;+x64;+rv:57.0)+Gecko/20100101+Firefox/57.0', - 'iis.access.user_agent.os': 'Windows', - 'iis.access.user_agent.os_name': 'Windows', - 'iis.access.user_name': '-', - 'iis.access.win32_status': '0', - 'input.type': 'log', - offset: 257, - 'prospector.type': 'log', + ], + 'iis.access.user_agent.os': ['Windows'], + 'iis.access.user_agent.os_name': ['Windows'], + 'iis.access.user_name': ['-'], + 'iis.access.win32_status': ['0'], + 'input.type': ['log'], + offset: [257], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[iis][access] ", - }, - Object { - "field": "iis.access.remote_ip", - "highlights": Array [], - "value": "85.181.35.98", - }, - Object { - "constant": " ", - }, - Object { - "field": "iis.access.user_name", - "highlights": Array [], - "value": "-", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "iis.access.method", - "highlights": Array [], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "iis.access.url", - "highlights": Array [], - "value": "/", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "iis.access.http_version", - "highlights": Array [], - "value": "undefined", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "iis.access.response_code", - "highlights": Array [], - "value": "200", - }, - Object { - "constant": " ", - }, - Object { - "field": "iis.access.body_sent.bytes", - "highlights": Array [], - "value": "undefined", - }, -] -`); + Array [ + Object { + "constant": "[iis][access] ", + }, + Object { + "field": "iis.access.remote_ip", + "highlights": Array [], + "value": Array [ + "85.181.35.98", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "iis.access.user_name", + "highlights": Array [], + "value": Array [ + "-", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "iis.access.method", + "highlights": Array [], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "iis.access.url", + "highlights": Array [], + "value": Array [ + "/", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "iis.access.http_version", + "highlights": Array [], + "value": Array [], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "iis.access.response_code", + "highlights": Array [], + "value": Array [ + "200", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "iis.access.body_sent.bytes", + "highlights": Array [], + "value": Array [], + }, + ] + `); }); test('iis 7.5 access log', () => { const flattenedDocument = { - '@timestamp': '2018-08-28T18:24:25.000Z', - 'event.dataset': 'iis.access', - 'fileset.module': 'iis', - 'fileset.name': 'access', - 'iis.access.method': 'GET', - 'iis.access.port': '80', - 'iis.access.query_string': '-', - 'iis.access.remote_ip': '10.100.118.31', - 'iis.access.request_time_ms': '792', - 'iis.access.response_code': '404', - 'iis.access.server_ip': '10.100.220.70', - 'iis.access.sub_status': '4', - 'iis.access.url': '/', - 'iis.access.user_agent.device': 'Other', - 'iis.access.user_agent.name': 'Other', - 'iis.access.user_agent.original': + '@timestamp': ['2018-08-28T18:24:25.000Z'], + 'event.dataset': ['iis.access'], + 'fileset.module': ['iis'], + 'fileset.name': ['access'], + 'iis.access.method': ['GET'], + 'iis.access.port': ['80'], + 'iis.access.query_string': ['-'], + 'iis.access.remote_ip': ['10.100.118.31'], + 'iis.access.request_time_ms': ['792'], + 'iis.access.response_code': ['404'], + 'iis.access.server_ip': ['10.100.220.70'], + 'iis.access.sub_status': ['4'], + 'iis.access.url': ['/'], + 'iis.access.user_agent.device': ['Other'], + 'iis.access.user_agent.name': ['Other'], + 'iis.access.user_agent.original': [ 'Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+6.3;+WOW64;+Trident/7.0;+.NET4.0E;+.NET4.0C;+.NET+CLR+3.5.30729;+.NET+CLR[+2.0.50727](tel:+2050727);+.NET+CLR+3.0.30729)', - 'iis.access.user_agent.os': 'Windows', - 'iis.access.user_agent.os_name': 'Windows', - 'iis.access.user_name': '-', - 'iis.access.win32_status': '2', - 'input.type': 'log', - offset: 244, - 'prospector.type': 'log', + ], + 'iis.access.user_agent.os': ['Windows'], + 'iis.access.user_agent.os_name': ['Windows'], + 'iis.access.user_name': ['-'], + 'iis.access.win32_status': ['2'], + 'input.type': ['log'], + offset: [244], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[iis][access] ", - }, - Object { - "field": "iis.access.remote_ip", - "highlights": Array [], - "value": "10.100.118.31", - }, - Object { - "constant": " ", - }, - Object { - "field": "iis.access.user_name", - "highlights": Array [], - "value": "-", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "iis.access.method", - "highlights": Array [], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "iis.access.url", - "highlights": Array [], - "value": "/", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "iis.access.http_version", - "highlights": Array [], - "value": "undefined", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "iis.access.response_code", - "highlights": Array [], - "value": "404", - }, - Object { - "constant": " ", - }, - Object { - "field": "iis.access.body_sent.bytes", - "highlights": Array [], - "value": "undefined", - }, -] -`); + Array [ + Object { + "constant": "[iis][access] ", + }, + Object { + "field": "iis.access.remote_ip", + "highlights": Array [], + "value": Array [ + "10.100.118.31", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "iis.access.user_name", + "highlights": Array [], + "value": Array [ + "-", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "iis.access.method", + "highlights": Array [], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "iis.access.url", + "highlights": Array [], + "value": Array [ + "/", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "iis.access.http_version", + "highlights": Array [], + "value": Array [], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "iis.access.response_code", + "highlights": Array [], + "value": Array [ + "404", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "iis.access.body_sent.bytes", + "highlights": Array [], + "value": Array [], + }, + ] + `); }); test('iis error log', () => { const flattenedDocument = { - '@timestamp': '2018-01-01T08:09:10.000Z', - 'event.dataset': 'iis.error', - 'fileset.module': 'iis', - 'fileset.name': 'error', - 'iis.error.http_version': '1.1', - 'iis.error.method': 'GET', - 'iis.error.queue_name': '-', - 'iis.error.reason_phrase': 'ConnLimit', - 'iis.error.remote_ip': '172.31.77.6', - 'iis.error.remote_port': '2094', - 'iis.error.response_code': '503', - 'iis.error.server_ip': '172.31.77.6', - 'iis.error.server_port': '80', - 'iis.error.url': '/qos/1kbfile.txt', - 'input.type': 'log', - offset: 186, - 'prospector.type': 'log', + '@timestamp': ['2018-01-01T08:09:10.000Z'], + 'event.dataset': ['iis.error'], + 'fileset.module': ['iis'], + 'fileset.name': ['error'], + 'iis.error.http_version': ['1.1'], + 'iis.error.method': ['GET'], + 'iis.error.queue_name': ['-'], + 'iis.error.reason_phrase': ['ConnLimit'], + 'iis.error.remote_ip': ['172.31.77.6'], + 'iis.error.remote_port': ['2094'], + 'iis.error.response_code': ['503'], + 'iis.error.server_ip': ['172.31.77.6'], + 'iis.error.server_port': ['80'], + 'iis.error.url': ['/qos/1kbfile.txt'], + 'input.type': ['log'], + offset: [186], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[iis][error] ", - }, - Object { - "field": "iis.error.remote_ip", - "highlights": Array [], - "value": "172.31.77.6", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "iis.error.method", - "highlights": Array [], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "iis.error.url", - "highlights": Array [], - "value": "/qos/1kbfile.txt", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "iis.error.http_version", - "highlights": Array [], - "value": "1.1", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "iis.error.response_code", - "highlights": Array [], - "value": "503", - }, - Object { - "constant": " ", - }, - Object { - "field": "iis.error.reason_phrase", - "highlights": Array [], - "value": "ConnLimit", - }, -] -`); + Array [ + Object { + "constant": "[iis][error] ", + }, + Object { + "field": "iis.error.remote_ip", + "highlights": Array [], + "value": Array [ + "172.31.77.6", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "iis.error.method", + "highlights": Array [], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "iis.error.url", + "highlights": Array [], + "value": Array [ + "/qos/1kbfile.txt", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "iis.error.http_version", + "highlights": Array [], + "value": Array [ + "1.1", + ], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "iis.error.response_code", + "highlights": Array [], + "value": Array [ + "503", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "iis.error.reason_phrase", + "highlights": Array [], + "value": Array [ + "ConnLimit", + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_kafka.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_kafka.test.ts index 19cb5f6e31118..4ea3cec8e91f6 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_kafka.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_kafka.test.ts @@ -13,48 +13,54 @@ describe('Filebeat Rules', () => { describe('in ECS format', () => { test('kafka log', () => { const flattenedDocument = { - '@timestamp': '2017-08-04T10:48:21.063Z', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'kafka.log', - 'event.module': 'kafka', - 'fileset.name': 'log', - 'input.type': 'log', - 'kafka.log.class': 'kafka.controller.KafkaController', - 'kafka.log.component': 'Controller 0', - 'log.level': 'INFO', - 'log.offset': 131, - message: '0 successfully elected as the controller', - 'service.type': 'kafka', + '@timestamp': ['2017-08-04T10:48:21.063Z'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['kafka.log'], + 'event.module': ['kafka'], + 'fileset.name': ['log'], + 'input.type': ['log'], + 'kafka.log.class': ['kafka.controller.KafkaController'], + 'kafka.log.component': ['Controller 0'], + 'log.level': ['INFO'], + 'log.offset': [131], + message: ['0 successfully elected as the controller'], + 'service.type': ['kafka'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "event.dataset", - "highlights": Array [], - "value": "kafka.log", - }, - Object { - "constant": "][", - }, - Object { - "field": "log.level", - "highlights": Array [], - "value": "INFO", - }, - Object { - "constant": "] ", - }, - Object { - "field": "message", - "highlights": Array [], - "value": "0 successfully elected as the controller", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "event.dataset", + "highlights": Array [], + "value": Array [ + "kafka.log", + ], + }, + Object { + "constant": "][", + }, + Object { + "field": "log.level", + "highlights": Array [], + "value": Array [ + "INFO", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "message", + "highlights": Array [], + "value": Array [ + "0 successfully elected as the controller", + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_logstash.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_logstash.test.ts index edc534d9c345f..022ea4921d9ad 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_logstash.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_logstash.test.ts @@ -13,194 +13,256 @@ describe('Filebeat Rules', () => { describe('in ECS format', () => { test('logstash log', () => { const flattenedDocument = { - '@timestamp': '2017-10-23T14:20:12.046Z', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'logstash.log', - 'event.module': 'logstash', - 'fileset.name': 'log', - 'input.type': 'log', - 'log.level': 'INFO', - 'log.offset': 0, - 'logstash.log.module': 'logstash.modules.scaffold', - message: + '@timestamp': ['2017-10-23T14:20:12.046Z'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['logstash.log'], + 'event.module': ['logstash'], + 'fileset.name': ['log'], + 'input.type': ['log'], + 'log.level': ['INFO'], + 'log.offset': [0], + 'logstash.log.module': ['logstash.modules.scaffold'], + message: [ 'Initializing module {:module_name=>"fb_apache", :directory=>"/usr/share/logstash/modules/fb_apache/configuration"}', - 'service.type': 'logstash', + ], + 'service.type': ['logstash'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "event.dataset", - "highlights": Array [], - "value": "logstash.log", - }, - Object { - "constant": "][", - }, - Object { - "field": "log.level", - "highlights": Array [], - "value": "INFO", - }, - Object { - "constant": "] ", - }, - Object { - "field": "message", - "highlights": Array [], - "value": "Initializing module {:module_name=>\\"fb_apache\\", :directory=>\\"/usr/share/logstash/modules/fb_apache/configuration\\"}", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "event.dataset", + "highlights": Array [], + "value": Array [ + "logstash.log", + ], + }, + Object { + "constant": "][", + }, + Object { + "field": "log.level", + "highlights": Array [], + "value": Array [ + "INFO", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "message", + "highlights": Array [], + "value": Array [ + "Initializing module {:module_name=>\\"fb_apache\\", :directory=>\\"/usr/share/logstash/modules/fb_apache/configuration\\"}", + ], + }, + ] + `); }); test('logstash slowlog', () => { const flattenedDocument = { - '@timestamp': '2017-10-30T09:57:58.243Z', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'logstash.slowlog', - 'event.duration': 3027675106, - 'event.module': 'logstash', - 'fileset.name': 'slowlog', - 'input.type': 'log', - 'log.level': 'WARN', - 'log.offset': 0, - 'logstash.slowlog': { - event: - '"{\\"@version\\":\\"1\\",\\"@timestamp\\":\\"2017-10-30T13:57:55.130Z\\",\\"host\\":\\"sashimi\\",\\"sequence\\":0,\\"message\\":\\"Hello world!\\"}"', - module: 'slowlog.logstash.filters.sleep', - plugin_name: 'sleep', - plugin_params: - '{"time"=>3, "id"=>"e4e12a4e3082615c5427079bf4250dbfa338ebac10f8ea9912d7b98a14f56b8c"}', - plugin_type: 'filters', - took_in_millis: 3027, - }, - 'service.type': 'logstash', + '@timestamp': ['2017-10-30T09:57:58.243Z'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['logstash.slowlog'], + 'event.duration': [3027675106], + 'event.module': ['logstash'], + 'fileset.name': ['slowlog'], + 'input.type': ['log'], + 'log.level': ['WARN'], + 'log.offset': [0], + 'logstash.slowlog.event': [ + '"{\\"@version\\":\\"1\\",\\"@timestamp\\":\\"2017-10-30T13:57:55.130Z\\",\\"host\\":\\"sashimi\\",\\"sequence\\":0,\\"message\\":\\"Hello world!\\"}"', + ], + 'logstash.slowlog.module': ['slowlog.logstash.filters.sleep'], + 'logstash.slowlog.plugin_name': ['sleep'], + 'logstash.slowlog.plugin_params': [ + '{"time"=>3, "id"=>"e4e12a4e3082615c5427079bf4250dbfa338ebac10f8ea9912d7b98a14f56b8c"}', + ], + 'logstash.slowlog.plugin_type': ['filters'], + 'logstash.slowlog.took_in_millis': [3027], + 'service.type': ['logstash'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[Logstash][", - }, - Object { - "field": "log.level", - "highlights": Array [], - "value": "WARN", - }, - Object { - "constant": "] ", - }, - Object { - "field": "logstash.slowlog", - "highlights": Array [], - "value": "{\\"event\\":\\"\\\\\\"{\\\\\\\\\\\\\\"@version\\\\\\\\\\\\\\":\\\\\\\\\\\\\\"1\\\\\\\\\\\\\\",\\\\\\\\\\\\\\"@timestamp\\\\\\\\\\\\\\":\\\\\\\\\\\\\\"2017-10-30T13:57:55.130Z\\\\\\\\\\\\\\",\\\\\\\\\\\\\\"host\\\\\\\\\\\\\\":\\\\\\\\\\\\\\"sashimi\\\\\\\\\\\\\\",\\\\\\\\\\\\\\"sequence\\\\\\\\\\\\\\":0,\\\\\\\\\\\\\\"message\\\\\\\\\\\\\\":\\\\\\\\\\\\\\"Hello world!\\\\\\\\\\\\\\"}\\\\\\"\\",\\"module\\":\\"slowlog.logstash.filters.sleep\\",\\"plugin_name\\":\\"sleep\\",\\"plugin_params\\":\\"{\\\\\\"time\\\\\\"=>3, \\\\\\"id\\\\\\"=>\\\\\\"e4e12a4e3082615c5427079bf4250dbfa338ebac10f8ea9912d7b98a14f56b8c\\\\\\"}\\",\\"plugin_type\\":\\"filters\\",\\"took_in_millis\\":3027}", - }, -] -`); + Array [ + Object { + "constant": "[Logstash][", + }, + Object { + "field": "log.level", + "highlights": Array [], + "value": Array [ + "WARN", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "logstash.slowlog.event", + "highlights": Array [], + "value": Array [ + "\\"{\\\\\\"@version\\\\\\":\\\\\\"1\\\\\\",\\\\\\"@timestamp\\\\\\":\\\\\\"2017-10-30T13:57:55.130Z\\\\\\",\\\\\\"host\\\\\\":\\\\\\"sashimi\\\\\\",\\\\\\"sequence\\\\\\":0,\\\\\\"message\\\\\\":\\\\\\"Hello world!\\\\\\"}\\"", + ], + }, + Object { + "field": "logstash.slowlog.module", + "highlights": Array [], + "value": Array [ + "slowlog.logstash.filters.sleep", + ], + }, + Object { + "field": "logstash.slowlog.plugin_name", + "highlights": Array [], + "value": Array [ + "sleep", + ], + }, + Object { + "field": "logstash.slowlog.plugin_params", + "highlights": Array [], + "value": Array [ + "{\\"time\\"=>3, \\"id\\"=>\\"e4e12a4e3082615c5427079bf4250dbfa338ebac10f8ea9912d7b98a14f56b8c\\"}", + ], + }, + Object { + "field": "logstash.slowlog.plugin_type", + "highlights": Array [], + "value": Array [ + "filters", + ], + }, + Object { + "field": "logstash.slowlog.took_in_millis", + "highlights": Array [], + "value": Array [ + 3027, + ], + }, + ] + `); }); }); describe('in pre-ECS format', () => { test('logstash log', () => { const flattenedDocument = { - '@timestamp': '2017-10-23T14:20:12.046Z', - 'event.dataset': 'logstash.log', - 'fileset.module': 'logstash', - 'fileset.name': 'log', - 'input.type': 'log', - 'logstash.log.level': 'INFO', - 'logstash.log.message': + '@timestamp': ['2017-10-23T14:20:12.046Z'], + 'event.dataset': ['logstash.log'], + 'fileset.module': ['logstash'], + 'fileset.name': ['log'], + 'input.type': ['log'], + 'logstash.log.level': ['INFO'], + 'logstash.log.message': [ 'Initializing module {:module_name=>"fb_apache", :directory=>"/usr/share/logstash/modules/fb_apache/configuration"}', - 'logstash.log.module': 'logstash.modules.scaffold', - offset: 0, - 'prospector.type': 'log', + ], + 'logstash.log.module': ['logstash.modules.scaffold'], + offset: [0], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[Logstash][", - }, - Object { - "field": "logstash.log.level", - "highlights": Array [], - "value": "INFO", - }, - Object { - "constant": "] ", - }, - Object { - "field": "logstash.log.module", - "highlights": Array [], - "value": "logstash.modules.scaffold", - }, - Object { - "constant": " - ", - }, - Object { - "field": "logstash.log.message", - "highlights": Array [], - "value": "Initializing module {:module_name=>\\"fb_apache\\", :directory=>\\"/usr/share/logstash/modules/fb_apache/configuration\\"}", - }, -] -`); + Array [ + Object { + "constant": "[Logstash][", + }, + Object { + "field": "logstash.log.level", + "highlights": Array [], + "value": Array [ + "INFO", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "logstash.log.module", + "highlights": Array [], + "value": Array [ + "logstash.modules.scaffold", + ], + }, + Object { + "constant": " - ", + }, + Object { + "field": "logstash.log.message", + "highlights": Array [], + "value": Array [ + "Initializing module {:module_name=>\\"fb_apache\\", :directory=>\\"/usr/share/logstash/modules/fb_apache/configuration\\"}", + ], + }, + ] + `); }); test('logstash slowlog', () => { const flattenedDocument = { - '@timestamp': '2017-10-30T09:57:58.243Z', - 'event.dataset': 'logstash.slowlog', - 'fileset.module': 'logstash', - 'fileset.name': 'slowlog', - 'input.type': 'log', - 'logstash.slowlog.event': + '@timestamp': ['2017-10-30T09:57:58.243Z'], + 'event.dataset': ['logstash.slowlog'], + 'fileset.module': ['logstash'], + 'fileset.name': ['slowlog'], + 'input.type': ['log'], + 'logstash.slowlog.event': [ '"{\\"@version\\":\\"1\\",\\"@timestamp\\":\\"2017-10-30T13:57:55.130Z\\",\\"host\\":\\"sashimi\\",\\"sequence\\":0,\\"message\\":\\"Hello world!\\"}"', - 'logstash.slowlog.level': 'WARN', - 'logstash.slowlog.message': + ], + 'logstash.slowlog.level': ['WARN'], + 'logstash.slowlog.message': [ 'event processing time {:plugin_params=>{"time"=>3, "id"=>"e4e12a4e3082615c5427079bf4250dbfa338ebac10f8ea9912d7b98a14f56b8c"}, :took_in_nanos=>3027675106, :took_in_millis=>3027, :event=>"{\\"@version\\":\\"1\\",\\"@timestamp\\":\\"2017-10-30T13:57:55.130Z\\",\\"host\\":\\"sashimi\\",\\"sequence\\":0,\\"message\\":\\"Hello world!\\"}"}', - 'logstash.slowlog.module': 'slowlog.logstash.filters.sleep', - 'logstash.slowlog.plugin_name': 'sleep', - 'logstash.slowlog.plugin_params': + ], + 'logstash.slowlog.module': ['slowlog.logstash.filters.sleep'], + 'logstash.slowlog.plugin_name': ['sleep'], + 'logstash.slowlog.plugin_params': [ '{"time"=>3, "id"=>"e4e12a4e3082615c5427079bf4250dbfa338ebac10f8ea9912d7b98a14f56b8c"}', - 'logstash.slowlog.plugin_type': 'filters', - 'logstash.slowlog.took_in_millis': 3027, - 'logstash.slowlog.took_in_nanos': 3027675106, - offset: 0, - 'prospector.type': 'log', + ], + 'logstash.slowlog.plugin_type': ['filters'], + 'logstash.slowlog.took_in_millis': [3027], + 'logstash.slowlog.took_in_nanos': [3027675106], + offset: [0], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[Logstash][", - }, - Object { - "field": "logstash.slowlog.level", - "highlights": Array [], - "value": "WARN", - }, - Object { - "constant": "] ", - }, - Object { - "field": "logstash.slowlog.module", - "highlights": Array [], - "value": "slowlog.logstash.filters.sleep", - }, - Object { - "constant": " - ", - }, - Object { - "field": "logstash.slowlog.message", - "highlights": Array [], - "value": "event processing time {:plugin_params=>{\\"time\\"=>3, \\"id\\"=>\\"e4e12a4e3082615c5427079bf4250dbfa338ebac10f8ea9912d7b98a14f56b8c\\"}, :took_in_nanos=>3027675106, :took_in_millis=>3027, :event=>\\"{\\\\\\"@version\\\\\\":\\\\\\"1\\\\\\",\\\\\\"@timestamp\\\\\\":\\\\\\"2017-10-30T13:57:55.130Z\\\\\\",\\\\\\"host\\\\\\":\\\\\\"sashimi\\\\\\",\\\\\\"sequence\\\\\\":0,\\\\\\"message\\\\\\":\\\\\\"Hello world!\\\\\\"}\\"}", - }, -] -`); + Array [ + Object { + "constant": "[Logstash][", + }, + Object { + "field": "logstash.slowlog.level", + "highlights": Array [], + "value": Array [ + "WARN", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "logstash.slowlog.module", + "highlights": Array [], + "value": Array [ + "slowlog.logstash.filters.sleep", + ], + }, + Object { + "constant": " - ", + }, + Object { + "field": "logstash.slowlog.message", + "highlights": Array [], + "value": Array [ + "event processing time {:plugin_params=>{\\"time\\"=>3, \\"id\\"=>\\"e4e12a4e3082615c5427079bf4250dbfa338ebac10f8ea9912d7b98a14f56b8c\\"}, :took_in_nanos=>3027675106, :took_in_millis=>3027, :event=>\\"{\\\\\\"@version\\\\\\":\\\\\\"1\\\\\\",\\\\\\"@timestamp\\\\\\":\\\\\\"2017-10-30T13:57:55.130Z\\\\\\",\\\\\\"host\\\\\\":\\\\\\"sashimi\\\\\\",\\\\\\"sequence\\\\\\":0,\\\\\\"message\\\\\\":\\\\\\"Hello world!\\\\\\"}\\"}", + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_logstash.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_logstash.ts index 39b2058ca7cdb..9471465183670 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_logstash.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_logstash.ts @@ -34,7 +34,7 @@ export const filebeatLogstashRules = [ { // ECS when: { - exists: ['ecs.version', 'logstash.slowlog'], + all: [{ exists: ['ecs.version'] }, { existsPrefix: ['logstash.slowlog'] }], }, format: [ { @@ -47,7 +47,7 @@ export const filebeatLogstashRules = [ constant: '] ', }, { - field: 'logstash.slowlog', + fieldsPrefix: 'logstash.slowlog', }, ], }, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_mongodb.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_mongodb.test.ts index 3df7ebec241cc..108ec29f550d2 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_mongodb.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_mongodb.test.ts @@ -13,40 +13,45 @@ describe('Filebeat Rules', () => { describe('in pre-ECS format', () => { test('mongodb log', () => { const flattenedDocument = { - '@timestamp': '2018-02-05T12:44:56.677Z', - 'event.dataset': 'mongodb.log', - 'fileset.module': 'mongodb', - 'fileset.name': 'log', - 'input.type': 'log', - 'mongodb.log.component': 'STORAGE', - 'mongodb.log.context': 'initandlisten', - 'mongodb.log.message': + '@timestamp': ['2018-02-05T12:44:56.677Z'], + 'event.dataset': ['mongodb.log'], + 'fileset.module': ['mongodb'], + 'fileset.name': ['log'], + 'input.type': ['log'], + 'mongodb.log.component': ['STORAGE'], + 'mongodb.log.context': ['initandlisten'], + 'mongodb.log.message': [ 'wiredtiger_open config: create,cache_size=8G,session_max=20000,eviction=(threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000),checkpoint=(wait=60,log_size=2GB),statistics_log=(wait=0),', - 'mongodb.log.severity': 'I', - offset: 281, - 'prospector.type': 'log', + ], + 'mongodb.log.severity': ['I'], + offset: [281], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[MongoDB][", - }, - Object { - "field": "mongodb.log.component", - "highlights": Array [], - "value": "STORAGE", - }, - Object { - "constant": "] ", - }, - Object { - "field": "mongodb.log.message", - "highlights": Array [], - "value": "wiredtiger_open config: create,cache_size=8G,session_max=20000,eviction=(threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000),checkpoint=(wait=60,log_size=2GB),statistics_log=(wait=0),", - }, -] -`); + Array [ + Object { + "constant": "[MongoDB][", + }, + Object { + "field": "mongodb.log.component", + "highlights": Array [], + "value": Array [ + "STORAGE", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "mongodb.log.message", + "highlights": Array [], + "value": Array [ + "wiredtiger_open config: create,cache_size=8G,session_max=20000,eviction=(threads_max=4),config_base=false,statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000),checkpoint=(wait=60,log_size=2GB),statistics_log=(wait=0),", + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_mysql.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_mysql.test.ts index 0329d53f92d08..99e83e9602480 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_mysql.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_mysql.test.ts @@ -13,139 +13,158 @@ describe('Filebeat Rules', () => { describe('in ECS format', () => { test('mysql error log', () => { const flattenedDocument = { - '@timestamp': '2016-12-09T12:08:33.335Z', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'mysql.error', - 'event.module': 'mysql', - 'fileset.name': 'error', - 'input.type': 'log', - 'log.level': 'Warning', - 'log.offset': 92, - message: + '@timestamp': ['2016-12-09T12:08:33.335Z'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['mysql.error'], + 'event.module': ['mysql'], + 'fileset.name': ['error'], + 'input.type': ['log'], + 'log.level': ['Warning'], + 'log.offset': [92], + message: [ 'TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).', - 'mysql.thread_id': 0, - 'service.type': 'mysql', + ], + 'mysql.thread_id': [0], + 'service.type': ['mysql'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "event.dataset", - "highlights": Array [], - "value": "mysql.error", - }, - Object { - "constant": "][", - }, - Object { - "field": "log.level", - "highlights": Array [], - "value": "Warning", - }, - Object { - "constant": "] ", - }, - Object { - "field": "message", - "highlights": Array [], - "value": "TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "event.dataset", + "highlights": Array [], + "value": Array [ + "mysql.error", + ], + }, + Object { + "constant": "][", + }, + Object { + "field": "log.level", + "highlights": Array [], + "value": Array [ + "Warning", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "message", + "highlights": Array [], + "value": Array [ + "TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).", + ], + }, + ] + `); }); test('mysql slowlog', () => { const flattenedDocument = { - '@timestamp': '2018-08-07T08:27:47.000Z', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'mysql.slowlog', - 'event.duration': 4071491000, - 'event.module': 'mysql', - 'fileset.name': 'slowlog', - 'input.type': 'log', + '@timestamp': ['2018-08-07T08:27:47.000Z'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['mysql.slowlog'], + 'event.duration': [4071491000], + 'event.module': ['mysql'], + 'fileset.name': ['slowlog'], + 'input.type': ['log'], 'log.flags': ['multiline'], - 'log.offset': 526, - 'mysql.slowlog.current_user': 'appuser', - 'mysql.slowlog.lock_time.sec': 0.000212, - 'mysql.slowlog.query': + 'log.offset': [526], + 'mysql.slowlog.current_user': ['appuser'], + 'mysql.slowlog.lock_time.sec': [0.000212], + 'mysql.slowlog.query': [ 'SELECT mcu.mcu_guid, mcu.cus_guid, mcu.mcu_url, mcu.mcu_crawlelements, mcu.mcu_order, GROUP_CONCAT(mca.mca_guid SEPARATOR ";") as mca_guid\n FROM kat_mailcustomerurl mcu, kat_customer cus, kat_mailcampaign mca\n WHERE cus.cus_guid = mcu.cus_guid\n AND cus.pro_code = \'CYB\'\n AND cus.cus_offline = 0\n AND mca.cus_guid = cus.cus_guid\n AND (mcu.mcu_date IS NULL OR mcu.mcu_date < CURDATE())\n AND mcu.mcu_crawlelements IS NOT NULL\n GROUP BY mcu.mcu_guid\n ORDER BY mcu.mcu_order ASC\n LIMIT 1000;', - 'mysql.slowlog.rows_examined': 1489615, - 'mysql.slowlog.rows_sent': 1000, - 'mysql.thread_id': 10997316, - 'service.type': 'mysql', - 'source.domain': 'apphost', - 'source.ip': '1.1.1.1', - 'user.name': 'appuser', + ], + 'mysql.slowlog.rows_examined': [1489615], + 'mysql.slowlog.rows_sent': [1000], + 'mysql.thread_id': [10997316], + 'service.type': ['mysql'], + 'source.domain': ['apphost'], + 'source.ip': ['1.1.1.1'], + 'user.name': ['appuser'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[MySQL][slowlog] ", - }, - Object { - "field": "user.name", - "highlights": Array [], - "value": "appuser", - }, - Object { - "constant": "@", - }, - Object { - "field": "source.domain", - "highlights": Array [], - "value": "apphost", - }, - Object { - "constant": " [", - }, - Object { - "field": "source.ip", - "highlights": Array [], - "value": "1.1.1.1", - }, - Object { - "constant": "] ", - }, - Object { - "constant": " - ", - }, - Object { - "field": "event.duration", - "highlights": Array [], - "value": "4071491000", - }, - Object { - "constant": " ns - ", - }, - Object { - "field": "mysql.slowlog.query", - "highlights": Array [], - "value": "SELECT mcu.mcu_guid, mcu.cus_guid, mcu.mcu_url, mcu.mcu_crawlelements, mcu.mcu_order, GROUP_CONCAT(mca.mca_guid SEPARATOR \\";\\") as mca_guid - FROM kat_mailcustomerurl mcu, kat_customer cus, kat_mailcampaign mca - WHERE cus.cus_guid = mcu.cus_guid - AND cus.pro_code = 'CYB' - AND cus.cus_offline = 0 - AND mca.cus_guid = cus.cus_guid - AND (mcu.mcu_date IS NULL OR mcu.mcu_date < CURDATE()) - AND mcu.mcu_crawlelements IS NOT NULL - GROUP BY mcu.mcu_guid - ORDER BY mcu.mcu_order ASC - LIMIT 1000;", - }, -] -`); + Array [ + Object { + "constant": "[MySQL][slowlog] ", + }, + Object { + "field": "user.name", + "highlights": Array [], + "value": Array [ + "appuser", + ], + }, + Object { + "constant": "@", + }, + Object { + "field": "source.domain", + "highlights": Array [], + "value": Array [ + "apphost", + ], + }, + Object { + "constant": " [", + }, + Object { + "field": "source.ip", + "highlights": Array [], + "value": Array [ + "1.1.1.1", + ], + }, + Object { + "constant": "] ", + }, + Object { + "constant": " - ", + }, + Object { + "field": "event.duration", + "highlights": Array [], + "value": Array [ + 4071491000, + ], + }, + Object { + "constant": " ns - ", + }, + Object { + "field": "mysql.slowlog.query", + "highlights": Array [], + "value": Array [ + "SELECT mcu.mcu_guid, mcu.cus_guid, mcu.mcu_url, mcu.mcu_crawlelements, mcu.mcu_order, GROUP_CONCAT(mca.mca_guid SEPARATOR \\";\\") as mca_guid + FROM kat_mailcustomerurl mcu, kat_customer cus, kat_mailcampaign mca + WHERE cus.cus_guid = mcu.cus_guid + AND cus.pro_code = 'CYB' + AND cus.cus_offline = 0 + AND mca.cus_guid = cus.cus_guid + AND (mcu.mcu_date IS NULL OR mcu.mcu_date < CURDATE()) + AND mcu.mcu_crawlelements IS NOT NULL + GROUP BY mcu.mcu_guid + ORDER BY mcu.mcu_order ASC + LIMIT 1000;", + ], + }, + ] + `); }); }); describe('in pre-ECS format', () => { test('mysql error log', () => { const errorDoc = { - 'mysql.error.message': + 'mysql.error.message': [ "Access denied for user 'petclinicdd'@'47.153.152.234' (using password: YES)", + ], }; const message = format(errorDoc, {}); expect(message).toEqual([ @@ -155,18 +174,18 @@ Array [ { field: 'mysql.error.message', highlights: [], - value: "Access denied for user 'petclinicdd'@'47.153.152.234' (using password: YES)", + value: ["Access denied for user 'petclinicdd'@'47.153.152.234' (using password: YES)"], }, ]); }); test('mysql slow log', () => { const errorDoc = { - 'mysql.slowlog.query': 'select * from hosts', - 'mysql.slowlog.query_time.sec': 5, - 'mysql.slowlog.user': 'admin', - 'mysql.slowlog.ip': '192.168.1.42', - 'mysql.slowlog.host': 'webserver-01', + 'mysql.slowlog.query': ['select * from hosts'], + 'mysql.slowlog.query_time.sec': [5], + 'mysql.slowlog.user': ['admin'], + 'mysql.slowlog.ip': ['192.168.1.42'], + 'mysql.slowlog.host': ['webserver-01'], }; const message = format(errorDoc, {}); expect(message).toEqual([ @@ -176,7 +195,7 @@ Array [ { field: 'mysql.slowlog.user', highlights: [], - value: 'admin', + value: ['admin'], }, { constant: '@', @@ -184,7 +203,7 @@ Array [ { field: 'mysql.slowlog.host', highlights: [], - value: 'webserver-01', + value: ['webserver-01'], }, { constant: ' [', @@ -192,7 +211,7 @@ Array [ { field: 'mysql.slowlog.ip', highlights: [], - value: '192.168.1.42', + value: ['192.168.1.42'], }, { constant: '] ', @@ -203,7 +222,7 @@ Array [ { field: 'mysql.slowlog.query_time.sec', highlights: [], - value: '5', + value: [5], }, { constant: ' s - ', @@ -211,7 +230,7 @@ Array [ { field: 'mysql.slowlog.query', highlights: [], - value: 'select * from hosts', + value: ['select * from hosts'], }, ]); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_nginx.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_nginx.test.ts index 0bc8ae1e907b8..62f48007eb0e3 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_nginx.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_nginx.test.ts @@ -13,252 +13,293 @@ describe('Filebeat Rules', () => { describe('in ECS format', () => { test('Nginx Access', () => { const flattenedDocument = { - '@timestamp': '2017-05-29T19:02:48.000Z', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'nginx.access', - 'event.module': 'nginx', - 'fileset.name': 'access', - 'http.request.method': 'GET', - 'http.request.referrer': '-', - 'http.response.body.bytes': 612, - 'http.response.status_code': 404, - 'http.version': '1.1', - 'input.type': 'log', - 'log.offset': 183, - 'service.type': 'nginx', - 'source.ip': '172.17.0.1', - 'url.original': '/stringpatch', - 'user.name': '-', - 'user_agent.device': 'Other', - 'user_agent.major': '15', - 'user_agent.minor': '0', - 'user_agent.name': 'Firefox Alpha', - 'user_agent.original': + '@timestamp': ['2017-05-29T19:02:48.000Z'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['nginx.access'], + 'event.module': ['nginx'], + 'fileset.name': ['access'], + 'http.request.method': ['GET'], + 'http.request.referrer': ['-'], + 'http.response.body.bytes': [612], + 'http.response.status_code': [404], + 'http.version': ['1.1'], + 'input.type': ['log'], + 'log.offset': [183], + 'service.type': ['nginx'], + 'source.ip': ['172.17.0.1'], + 'url.original': ['/stringpatch'], + 'user.name': ['-'], + 'user_agent.device': ['Other'], + 'user_agent.major': ['15'], + 'user_agent.minor': ['0'], + 'user_agent.name': ['Firefox Alpha'], + 'user_agent.original': [ 'Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20120716 Firefox/15.0a2', - 'user_agent.os.full_name': 'Windows 7', - 'user_agent.os.name': 'Windows 7', - 'user_agent.patch': 'a2', + ], + 'user_agent.os.full_name': ['Windows 7'], + 'user_agent.os.name': ['Windows 7'], + 'user_agent.patch': ['a2'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "event.module", - "highlights": Array [], - "value": "nginx", - }, - Object { - "constant": "][access] ", - }, - Object { - "field": "source.ip", - "highlights": Array [], - "value": "172.17.0.1", - }, - Object { - "constant": " ", - }, - Object { - "field": "user.name", - "highlights": Array [], - "value": "-", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "http.request.method", - "highlights": Array [], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "url.original", - "highlights": Array [], - "value": "/stringpatch", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "http.version", - "highlights": Array [], - "value": "1.1", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "http.response.status_code", - "highlights": Array [], - "value": "404", - }, - Object { - "constant": " ", - }, - Object { - "field": "http.response.body.bytes", - "highlights": Array [], - "value": "612", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "event.module", + "highlights": Array [], + "value": Array [ + "nginx", + ], + }, + Object { + "constant": "][access] ", + }, + Object { + "field": "source.ip", + "highlights": Array [], + "value": Array [ + "172.17.0.1", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "user.name", + "highlights": Array [], + "value": Array [ + "-", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "http.request.method", + "highlights": Array [], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "url.original", + "highlights": Array [], + "value": Array [ + "/stringpatch", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "http.version", + "highlights": Array [], + "value": Array [ + "1.1", + ], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "http.response.status_code", + "highlights": Array [], + "value": Array [ + 404, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "http.response.body.bytes", + "highlights": Array [], + "value": Array [ + 612, + ], + }, + ] + `); }); test('Nginx Error', () => { const flattenedDocument = { - '@timestamp': '2016-10-25T14:49:34.000Z', - 'ecs.version': '1.0.0-beta2', - 'event.dataset': 'nginx.error', - 'event.module': 'nginx', - 'fileset.name': 'error', - 'input.type': 'log', - 'log.level': 'error', - 'log.offset': 0, - message: + '@timestamp': ['2016-10-25T14:49:34.000Z'], + 'ecs.version': ['1.0.0-beta2'], + 'event.dataset': ['nginx.error'], + 'event.module': ['nginx'], + 'fileset.name': ['error'], + 'input.type': ['log'], + 'log.level': ['error'], + 'log.offset': [0], + message: [ 'open() "/usr/local/Cellar/nginx/1.10.2_1/html/favicon.ico" failed (2: No such file or directory), client: 127.0.0.1, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "localhost:8080", referrer: "http://localhost:8080/"', - 'nginx.error.connection_id': 1, - 'process.pid': 54053, - 'process.thread.id': 0, - 'service.type': 'nginx', + ], + 'nginx.error.connection_id': [1], + 'process.pid': [54053], + 'process.thread.id': [0], + 'service.type': ['nginx'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[nginx]", - }, - Object { - "constant": "[", - }, - Object { - "field": "log.level", - "highlights": Array [], - "value": "error", - }, - Object { - "constant": "] ", - }, - Object { - "field": "message", - "highlights": Array [], - "value": "open() \\"/usr/local/Cellar/nginx/1.10.2_1/html/favicon.ico\\" failed (2: No such file or directory), client: 127.0.0.1, server: localhost, request: \\"GET /favicon.ico HTTP/1.1\\", host: \\"localhost:8080\\", referrer: \\"http://localhost:8080/\\"", - }, -] -`); + Array [ + Object { + "constant": "[nginx]", + }, + Object { + "constant": "[", + }, + Object { + "field": "log.level", + "highlights": Array [], + "value": Array [ + "error", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "message", + "highlights": Array [], + "value": Array [ + "open() \\"/usr/local/Cellar/nginx/1.10.2_1/html/favicon.ico\\" failed (2: No such file or directory), client: 127.0.0.1, server: localhost, request: \\"GET /favicon.ico HTTP/1.1\\", host: \\"localhost:8080\\", referrer: \\"http://localhost:8080/\\"", + ], + }, + ] + `); }); }); describe('in pre-ECS format', () => { test('Nginx Access', () => { const flattenedDocument = { - 'nginx.access': true, - 'nginx.access.remote_ip': '192.168.1.42', - 'nginx.access.user_name': 'admin', - 'nginx.access.method': 'GET', - 'nginx.access.url': '/faq', - 'nginx.access.http_version': '1.1', - 'nginx.access.body_sent.bytes': 1024, - 'nginx.access.response_code': 200, + 'nginx.access': [true], + 'nginx.access.remote_ip': ['192.168.1.42'], + 'nginx.access.user_name': ['admin'], + 'nginx.access.method': ['GET'], + 'nginx.access.url': ['/faq'], + 'nginx.access.http_version': ['1.1'], + 'nginx.access.body_sent.bytes': [1024], + 'nginx.access.response_code': [200], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[nginx][access] ", - }, - Object { - "field": "nginx.access.remote_ip", - "highlights": Array [], - "value": "192.168.1.42", - }, - Object { - "constant": " ", - }, - Object { - "field": "nginx.access.user_name", - "highlights": Array [], - "value": "admin", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "nginx.access.method", - "highlights": Array [], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "nginx.access.url", - "highlights": Array [], - "value": "/faq", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "nginx.access.http_version", - "highlights": Array [], - "value": "1.1", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "nginx.access.response_code", - "highlights": Array [], - "value": "200", - }, - Object { - "constant": " ", - }, - Object { - "field": "nginx.access.body_sent.bytes", - "highlights": Array [], - "value": "1024", - }, -] -`); + Array [ + Object { + "constant": "[nginx][access] ", + }, + Object { + "field": "nginx.access.remote_ip", + "highlights": Array [], + "value": Array [ + "192.168.1.42", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "nginx.access.user_name", + "highlights": Array [], + "value": Array [ + "admin", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "nginx.access.method", + "highlights": Array [], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "nginx.access.url", + "highlights": Array [], + "value": Array [ + "/faq", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "nginx.access.http_version", + "highlights": Array [], + "value": Array [ + "1.1", + ], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "nginx.access.response_code", + "highlights": Array [], + "value": Array [ + 200, + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "nginx.access.body_sent.bytes", + "highlights": Array [], + "value": Array [ + 1024, + ], + }, + ] + `); }); test('Nginx Error', () => { const flattenedDocument = { - 'nginx.error.message': + 'nginx.error.message': [ 'connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: localhost, request: "GET /php-status?json= HTTP/1.1", upstream: "fastcgi://[::1]:9000", host: "localhost"', - 'nginx.error.level': 'error', + ], + 'nginx.error.level': ['error'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[nginx]", - }, - Object { - "constant": "[", - }, - Object { - "field": "nginx.error.level", - "highlights": Array [], - "value": "error", - }, - Object { - "constant": "] ", - }, - Object { - "field": "nginx.error.message", - "highlights": Array [], - "value": "connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: localhost, request: \\"GET /php-status?json= HTTP/1.1\\", upstream: \\"fastcgi://[::1]:9000\\", host: \\"localhost\\"", - }, -] -`); + Array [ + Object { + "constant": "[nginx]", + }, + Object { + "constant": "[", + }, + Object { + "field": "nginx.error.level", + "highlights": Array [], + "value": Array [ + "error", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "nginx.error.message", + "highlights": Array [], + "value": Array [ + "connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: localhost, request: \\"GET /php-status?json= HTTP/1.1\\", upstream: \\"fastcgi://[::1]:9000\\", host: \\"localhost\\"", + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_osquery.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_osquery.test.ts index 8dc70053e2022..513a9a8fcd910 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_osquery.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_osquery.test.ts @@ -13,65 +13,139 @@ describe('Filebeat Rules', () => { describe('in pre-ECS format', () => { test('osquery result log', () => { const flattenedDocument = { - '@timestamp': '2017-12-28T14:40:08.000Z', - 'event.dataset': 'osquery.result', - 'fileset.module': 'osquery', - 'fileset.name': 'result', - 'input.type': 'log', - offset: 0, - 'osquery.result.action': 'removed', - 'osquery.result.calendar_time': 'Thu Dec 28 14:40:08 2017 UTC', - 'osquery.result.columns': { - blocks: '122061322', - blocks_available: '75966945', - blocks_free: '121274885', - blocks_size: '4096', - device: '/dev/disk1s4', - device_alias: '/dev/disk1s4', - flags: '345018372', - inodes: '9223372036854775807', - inodes_free: '9223372036854775804', - path: '/private/var/vm', - type: 'apfs', - }, - 'osquery.result.counter': '1', - 'osquery.result.decorations.host_uuid': '4AB2906D-5516-5794-AF54-86D1D7F533F3', - 'osquery.result.decorations.username': 'tsg', - 'osquery.result.epoch': '0', - 'osquery.result.host_identifier': '192-168-0-4.rdsnet.ro', - 'osquery.result.name': 'pack_it-compliance_mounts', - 'osquery.result.unix_time': '1514472008', - 'prospector.type': 'log', + '@timestamp': ['2017-12-28T14:40:08.000Z'], + 'event.dataset': ['osquery.result'], + 'fileset.module': ['osquery'], + 'fileset.name': ['result'], + 'input.type': ['log'], + offset: [0], + 'osquery.result.action': ['removed'], + 'osquery.result.calendar_time': ['Thu Dec 28 14:40:08 2017 UTC'], + 'osquery.result.columns.blocks': ['122061322'], + 'osquery.result.columns.blocks_available': ['75966945'], + 'osquery.result.columns.blocks_free': ['121274885'], + 'osquery.result.columns.blocks_size': ['4096'], + 'osquery.result.columns.device': ['/dev/disk1s4'], + 'osquery.result.columns.device_alias': ['/dev/disk1s4'], + 'osquery.result.columns.flags': ['345018372'], + 'osquery.result.columns.inodes': ['9223372036854775807'], + 'osquery.result.columns.inodes_free': ['9223372036854775804'], + 'osquery.result.columns.path': ['/private/var/vm'], + 'osquery.result.columns.type': ['apfs'], + 'osquery.result.counter': ['1'], + 'osquery.result.decorations.host_uuid': ['4AB2906D-5516-5794-AF54-86D1D7F533F3'], + 'osquery.result.decorations.username': ['tsg'], + 'osquery.result.epoch': ['0'], + 'osquery.result.host_identifier': ['192-168-0-4.rdsnet.ro'], + 'osquery.result.name': ['pack_it-compliance_mounts'], + 'osquery.result.unix_time': ['1514472008'], + 'prospector.type': ['log'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[Osquery][", - }, - Object { - "field": "osquery.result.action", - "highlights": Array [], - "value": "removed", - }, - Object { - "constant": "] ", - }, - Object { - "field": "osquery.result.host_identifier", - "highlights": Array [], - "value": "192-168-0-4.rdsnet.ro", - }, - Object { - "constant": " ", - }, - Object { - "field": "osquery.result.columns", - "highlights": Array [], - "value": "{\\"blocks\\":\\"122061322\\",\\"blocks_available\\":\\"75966945\\",\\"blocks_free\\":\\"121274885\\",\\"blocks_size\\":\\"4096\\",\\"device\\":\\"/dev/disk1s4\\",\\"device_alias\\":\\"/dev/disk1s4\\",\\"flags\\":\\"345018372\\",\\"inodes\\":\\"9223372036854775807\\",\\"inodes_free\\":\\"9223372036854775804\\",\\"path\\":\\"/private/var/vm\\",\\"type\\":\\"apfs\\"}", - }, -] -`); + Array [ + Object { + "constant": "[Osquery][", + }, + Object { + "field": "osquery.result.action", + "highlights": Array [], + "value": Array [ + "removed", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "osquery.result.host_identifier", + "highlights": Array [], + "value": Array [ + "192-168-0-4.rdsnet.ro", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "osquery.result.columns.blocks", + "highlights": Array [], + "value": Array [ + "122061322", + ], + }, + Object { + "field": "osquery.result.columns.blocks_available", + "highlights": Array [], + "value": Array [ + "75966945", + ], + }, + Object { + "field": "osquery.result.columns.blocks_free", + "highlights": Array [], + "value": Array [ + "121274885", + ], + }, + Object { + "field": "osquery.result.columns.blocks_size", + "highlights": Array [], + "value": Array [ + "4096", + ], + }, + Object { + "field": "osquery.result.columns.device", + "highlights": Array [], + "value": Array [ + "/dev/disk1s4", + ], + }, + Object { + "field": "osquery.result.columns.device_alias", + "highlights": Array [], + "value": Array [ + "/dev/disk1s4", + ], + }, + Object { + "field": "osquery.result.columns.flags", + "highlights": Array [], + "value": Array [ + "345018372", + ], + }, + Object { + "field": "osquery.result.columns.inodes", + "highlights": Array [], + "value": Array [ + "9223372036854775807", + ], + }, + Object { + "field": "osquery.result.columns.inodes_free", + "highlights": Array [], + "value": Array [ + "9223372036854775804", + ], + }, + Object { + "field": "osquery.result.columns.path", + "highlights": Array [], + "value": Array [ + "/private/var/vm", + ], + }, + Object { + "field": "osquery.result.columns.type", + "highlights": Array [], + "value": Array [ + "apfs", + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_osquery.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_osquery.ts index b3a6ee8c5cb47..638fd01d50c90 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_osquery.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_osquery.ts @@ -27,7 +27,7 @@ export const filebeatOsqueryRules = [ constant: ' ', }, { - field: 'osquery.result.columns', + fieldsPrefix: 'osquery.result.columns', }, ], }, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_traefik.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_traefik.test.ts index b19124558fdd0..d5d16f26a282a 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_traefik.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_traefik.test.ts @@ -13,112 +13,129 @@ describe('Filebeat Rules', () => { describe('in pre-ECS format', () => { test('traefik access log', () => { const flattenedDocument = { - '@timestamp': '2017-10-02T20:22:08.000Z', - 'event.dataset': 'traefik.access', - 'fileset.module': 'traefik', - 'fileset.name': 'access', - 'input.type': 'log', - offset: 280, - 'prospector.type': 'log', - 'traefik.access.backend_url': 'http://172.19.0.3:5601', - 'traefik.access.body_sent.bytes': 0, - 'traefik.access.duration': 3, - 'traefik.access.frontend_name': 'Host-host1', - 'traefik.access.geoip.city_name': 'Berlin', - 'traefik.access.geoip.continent_name': 'Europe', - 'traefik.access.geoip.country_iso_code': 'DE', - 'traefik.access.geoip.location.lat': 52.4908, - 'traefik.access.geoip.location.lon': 13.3275, - 'traefik.access.geoip.region_iso_code': 'DE-BE', - 'traefik.access.geoip.region_name': 'Land Berlin', - 'traefik.access.http_version': '1.1', - 'traefik.access.method': 'GET', - 'traefik.access.referrer': 'http://example.com/login', - 'traefik.access.remote_ip': '85.181.35.98', - 'traefik.access.request_count': 271, - 'traefik.access.response_code': '304', - 'traefik.access.url': '/ui/favicons/favicon.ico', - 'traefik.access.user_agent.device': 'Other', - 'traefik.access.user_agent.major': '61', - 'traefik.access.user_agent.minor': '0', - 'traefik.access.user_agent.name': 'Chrome', - 'traefik.access.user_agent.original': + '@timestamp': ['2017-10-02T20:22:08.000Z'], + 'event.dataset': ['traefik.access'], + 'fileset.module': ['traefik'], + 'fileset.name': ['access'], + 'input.type': ['log'], + offset: [280], + 'prospector.type': ['log'], + 'traefik.access.backend_url': ['http://172.19.0.3:5601'], + 'traefik.access.body_sent.bytes': [0], + 'traefik.access.duration': [3], + 'traefik.access.frontend_name': ['Host-host1'], + 'traefik.access.geoip.city_name': ['Berlin'], + 'traefik.access.geoip.continent_name': ['Europe'], + 'traefik.access.geoip.country_iso_code': ['DE'], + 'traefik.access.geoip.location.lat': [52.4908], + 'traefik.access.geoip.location.lon': [13.3275], + 'traefik.access.geoip.region_iso_code': ['DE-BE'], + 'traefik.access.geoip.region_name': ['Land Berlin'], + 'traefik.access.http_version': ['1.1'], + 'traefik.access.method': ['GET'], + 'traefik.access.referrer': ['http://example.com/login'], + 'traefik.access.remote_ip': ['85.181.35.98'], + 'traefik.access.request_count': [271], + 'traefik.access.response_code': ['304'], + 'traefik.access.url': ['/ui/favicons/favicon.ico'], + 'traefik.access.user_agent.device': ['Other'], + 'traefik.access.user_agent.major': ['61'], + 'traefik.access.user_agent.minor': ['0'], + 'traefik.access.user_agent.name': ['Chrome'], + 'traefik.access.user_agent.original': [ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', - 'traefik.access.user_agent.os': 'Linux', - 'traefik.access.user_agent.os_name': 'Linux', - 'traefik.access.user_agent.patch': '3163', - 'traefik.access.user_identifier': '-', - 'traefik.access.user_name': '-', + ], + 'traefik.access.user_agent.os': ['Linux'], + 'traefik.access.user_agent.os_name': ['Linux'], + 'traefik.access.user_agent.patch': ['3163'], + 'traefik.access.user_identifier': ['-'], + 'traefik.access.user_name': ['-'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[traefik][access] ", - }, - Object { - "field": "traefik.access.remote_ip", - "highlights": Array [], - "value": "85.181.35.98", - }, - Object { - "constant": " ", - }, - Object { - "field": "traefik.access.frontend_name", - "highlights": Array [], - "value": "Host-host1", - }, - Object { - "constant": " -> ", - }, - Object { - "field": "traefik.access.backend_url", - "highlights": Array [], - "value": "http://172.19.0.3:5601", - }, - Object { - "constant": " \\"", - }, - Object { - "field": "traefik.access.method", - "highlights": Array [], - "value": "GET", - }, - Object { - "constant": " ", - }, - Object { - "field": "traefik.access.url", - "highlights": Array [], - "value": "/ui/favicons/favicon.ico", - }, - Object { - "constant": " HTTP/", - }, - Object { - "field": "traefik.access.http_version", - "highlights": Array [], - "value": "1.1", - }, - Object { - "constant": "\\" ", - }, - Object { - "field": "traefik.access.response_code", - "highlights": Array [], - "value": "304", - }, - Object { - "constant": " ", - }, - Object { - "field": "traefik.access.body_sent.bytes", - "highlights": Array [], - "value": "0", - }, -] -`); + Array [ + Object { + "constant": "[traefik][access] ", + }, + Object { + "field": "traefik.access.remote_ip", + "highlights": Array [], + "value": Array [ + "85.181.35.98", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "traefik.access.frontend_name", + "highlights": Array [], + "value": Array [ + "Host-host1", + ], + }, + Object { + "constant": " -> ", + }, + Object { + "field": "traefik.access.backend_url", + "highlights": Array [], + "value": Array [ + "http://172.19.0.3:5601", + ], + }, + Object { + "constant": " \\"", + }, + Object { + "field": "traefik.access.method", + "highlights": Array [], + "value": Array [ + "GET", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "traefik.access.url", + "highlights": Array [], + "value": Array [ + "/ui/favicons/favicon.ico", + ], + }, + Object { + "constant": " HTTP/", + }, + Object { + "field": "traefik.access.http_version", + "highlights": Array [], + "value": Array [ + "1.1", + ], + }, + Object { + "constant": "\\" ", + }, + Object { + "field": "traefik.access.response_code", + "highlights": Array [], + "value": Array [ + "304", + ], + }, + Object { + "constant": " ", + }, + Object { + "field": "traefik.access.body_sent.bytes", + "highlights": Array [], + "value": Array [ + 0, + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/generic.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/generic.test.ts index d168273626cfa..eba0563be3aaa 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/generic.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/generic.test.ts @@ -15,154 +15,174 @@ describe('Generic Rules', () => { describe('configurable message rules', () => { test('includes the event.dataset and log.level if present', () => { const flattenedDocument = { - '@timestamp': '2016-12-26T16:22:13.000Z', - 'event.dataset': 'generic.test', - 'log.level': 'TEST_LEVEL', - first_generic_message: 'TEST_MESSAGE', + '@timestamp': ['2016-12-26T16:22:13.000Z'], + 'event.dataset': ['generic.test'], + 'log.level': ['TEST_LEVEL'], + first_generic_message: ['TEST_MESSAGE'], }; const highlights = { first_generic_message: ['TEST'], }; expect(format(flattenedDocument, highlights)).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "event.dataset", - "highlights": Array [], - "value": "generic.test", - }, - Object { - "constant": "][", - }, - Object { - "field": "log.level", - "highlights": Array [], - "value": "TEST_LEVEL", - }, - Object { - "constant": "] ", - }, - Object { - "field": "first_generic_message", - "highlights": Array [ - "TEST", - ], - "value": "TEST_MESSAGE", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "event.dataset", + "highlights": Array [], + "value": Array [ + "generic.test", + ], + }, + Object { + "constant": "][", + }, + Object { + "field": "log.level", + "highlights": Array [], + "value": Array [ + "TEST_LEVEL", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "first_generic_message", + "highlights": Array [ + "TEST", + ], + "value": Array [ + "TEST_MESSAGE", + ], + }, + ] + `); }); test('includes the log.level if present', () => { const flattenedDocument = { - '@timestamp': '2016-12-26T16:22:13.000Z', - 'log.level': 'TEST_LEVEL', - first_generic_message: 'TEST_MESSAGE', + '@timestamp': ['2016-12-26T16:22:13.000Z'], + 'log.level': ['TEST_LEVEL'], + first_generic_message: ['TEST_MESSAGE'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "log.level", - "highlights": Array [], - "value": "TEST_LEVEL", - }, - Object { - "constant": "] ", - }, - Object { - "field": "first_generic_message", - "highlights": Array [], - "value": "TEST_MESSAGE", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "log.level", + "highlights": Array [], + "value": Array [ + "TEST_LEVEL", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "first_generic_message", + "highlights": Array [], + "value": Array [ + "TEST_MESSAGE", + ], + }, + ] + `); }); test('includes the message', () => { const firstFlattenedDocument = { - '@timestamp': '2016-12-26T16:22:13.000Z', - first_generic_message: 'FIRST_TEST_MESSAGE', + '@timestamp': ['2016-12-26T16:22:13.000Z'], + first_generic_message: ['FIRST_TEST_MESSAGE'], }; expect(format(firstFlattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "field": "first_generic_message", - "highlights": Array [], - "value": "FIRST_TEST_MESSAGE", - }, -] -`); + Array [ + Object { + "field": "first_generic_message", + "highlights": Array [], + "value": Array [ + "FIRST_TEST_MESSAGE", + ], + }, + ] + `); const secondFlattenedDocument = { - '@timestamp': '2016-12-26T16:22:13.000Z', - second_generic_message: 'SECOND_TEST_MESSAGE', + '@timestamp': ['2016-12-26T16:22:13.000Z'], + second_generic_message: ['SECOND_TEST_MESSAGE'], }; expect(format(secondFlattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "field": "second_generic_message", - "highlights": Array [], - "value": "SECOND_TEST_MESSAGE", - }, -] -`); + Array [ + Object { + "field": "second_generic_message", + "highlights": Array [], + "value": Array [ + "SECOND_TEST_MESSAGE", + ], + }, + ] + `); }); }); describe('log.original fallback', () => { test('includes the event.dataset if present', () => { const flattenedDocument = { - '@timestamp': '2016-12-26T16:22:13.000Z', - 'event.dataset': 'generic.test', - 'log.original': 'TEST_MESSAGE', + '@timestamp': ['2016-12-26T16:22:13.000Z'], + 'event.dataset': ['generic.test'], + 'log.original': ['TEST_MESSAGE'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "constant": "[", - }, - Object { - "field": "event.dataset", - "highlights": Array [], - "value": "generic.test", - }, - Object { - "constant": "] ", - }, - Object { - "field": "log.original", - "highlights": Array [], - "value": "TEST_MESSAGE", - }, -] -`); + Array [ + Object { + "constant": "[", + }, + Object { + "field": "event.dataset", + "highlights": Array [], + "value": Array [ + "generic.test", + ], + }, + Object { + "constant": "] ", + }, + Object { + "field": "log.original", + "highlights": Array [], + "value": Array [ + "TEST_MESSAGE", + ], + }, + ] + `); }); test('includes the original message', () => { const flattenedDocument = { - '@timestamp': '2016-12-26T16:22:13.000Z', - 'log.original': 'TEST_MESSAGE', + '@timestamp': ['2016-12-26T16:22:13.000Z'], + 'log.original': ['TEST_MESSAGE'], }; expect(format(flattenedDocument, {})).toMatchInlineSnapshot(` -Array [ - Object { - "field": "log.original", - "highlights": Array [], - "value": "TEST_MESSAGE", - }, -] -`); + Array [ + Object { + "field": "log.original", + "highlights": Array [], + "value": Array [ + "TEST_MESSAGE", + ], + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/helpers.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/helpers.ts index 9a6fa30e17e89..e925fab75d984 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/helpers.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/helpers.ts @@ -10,3 +10,10 @@ export const labelField = (label: string, field: string) => [ { constant: '=' }, { field }, ]; + +export const labelFieldsPrefix = (label: string, fieldsPrefix: string) => [ + { constant: ' ' }, + { constant: label }, + { constant: '=' }, + { fieldsPrefix }, +]; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts index 98d1e2cd89b01..7b79a1bf0386a 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts @@ -4,66 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ -import { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields'; +import { convertESFieldsToLogItemFields } from './convert_document_source_to_log_item_fields'; -describe('convertDocumentSourceToLogItemFields', () => { - test('should convert document', () => { - const doc = { - agent: { - hostname: 'demo-stack-client-01', - id: '7adef8b6-2ab7-45cd-a0d5-b3baad735f1b', - type: 'filebeat', - ephemeral_id: 'a0c8164b-3564-4e32-b0bf-f4db5a7ae566', - version: '7.0.0', - }, +describe('convertESFieldsToLogItemFields', () => { + test('Converts the fields collection to LogItemFields', () => { + const esFields = { + 'agent.hostname': ['demo-stack-client-01'], + 'agent.id': ['7adef8b6-2ab7-45cd-a0d5-b3baad735f1b'], + 'agent.type': ['filebeat'], + 'agent.ephemeral_id': ['a0c8164b-3564-4e32-b0bf-f4db5a7ae566'], + 'agent.version': ['7.0.0'], tags: ['prod', 'web'], metadata: [ { key: 'env', value: 'prod' }, { key: 'stack', value: 'web' }, ], - host: { - hostname: 'packer-virtualbox-iso-1546820004', - name: 'demo-stack-client-01', - }, + 'host.hostname': ['packer-virtualbox-iso-1546820004'], + 'host.name': ['demo-stack-client-01'], }; - const fields = convertDocumentSourceToLogItemFields(doc); + const fields = convertESFieldsToLogItemFields(esFields); expect(fields).toEqual([ { field: 'agent.hostname', - value: 'demo-stack-client-01', + value: ['demo-stack-client-01'], }, { field: 'agent.id', - value: '7adef8b6-2ab7-45cd-a0d5-b3baad735f1b', + value: ['7adef8b6-2ab7-45cd-a0d5-b3baad735f1b'], }, { field: 'agent.type', - value: 'filebeat', + value: ['filebeat'], }, { field: 'agent.ephemeral_id', - value: 'a0c8164b-3564-4e32-b0bf-f4db5a7ae566', + value: ['a0c8164b-3564-4e32-b0bf-f4db5a7ae566'], }, { field: 'agent.version', - value: '7.0.0', + value: ['7.0.0'], }, { field: 'tags', - value: '["prod","web"]', + value: ['prod', 'web'], }, { field: 'metadata', - value: '[{"key":"env","value":"prod"},{"key":"stack","value":"web"}]', + value: ['{"key":"env","value":"prod"}', '{"key":"stack","value":"web"}'], }, { field: 'host.hostname', - value: 'packer-virtualbox-iso-1546820004', + value: ['packer-virtualbox-iso-1546820004'], }, { field: 'host.name', - value: 'demo-stack-client-01', + value: ['demo-stack-client-01'], }, ]); }); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts index 7c8560d72ff97..a1d855bfdaa48 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts @@ -5,39 +5,21 @@ */ import stringify from 'json-stable-stringify'; -import { isArray, isPlainObject } from 'lodash'; - -import { JsonObject } from '../../../../common/typed_json'; import { LogEntriesItemField } from '../../../../common/http_api'; +import { JsonArray } from '../../../../common/typed_json'; -const isJsonObject = (subject: any): subject is JsonObject => { - return isPlainObject(subject); +const serializeValue = (value: JsonArray): string[] => { + return value.map((v) => { + if (typeof v === 'object' && v != null) { + return stringify(v); + } else { + return `${v}`; + } + }); }; -const serializeValue = (value: any): string => { - if (isArray(value) || isPlainObject(value)) { - return stringify(value); - } - return `${value}`; -}; export const convertESFieldsToLogItemFields = (fields: { - [field: string]: [value: unknown]; + [field: string]: JsonArray; }): LogEntriesItemField[] => { - return Object.keys(fields).map((field) => ({ field, value: serializeValue(fields[field][0]) })); -}; - -export const convertDocumentSourceToLogItemFields = ( - source: JsonObject, - path: string[] = [], - fields: LogEntriesItemField[] = [] -): LogEntriesItemField[] => { - return Object.keys(source).reduce((acc, key) => { - const value = source[key]; - const nextPath = [...path, key]; - if (isJsonObject(value)) { - return convertDocumentSourceToLogItemFields(value, nextPath, acc); - } - const field = { field: nextPath.join('.'), value: serializeValue(value) }; - return [...acc, field]; - }, fields); + return Object.keys(fields).map((field) => ({ field, value: serializeValue(fields[field]) })); }; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index e211f72b4e076..cc9d4c749c77d 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -7,7 +7,7 @@ import { sortBy } from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; -import { JsonObject } from '../../../../common/typed_json'; +import { JsonArray, JsonObject } from '../../../../common/typed_json'; import { LogEntriesSummaryBucket, LogEntriesSummaryHighlightsBucket, @@ -163,8 +163,8 @@ export class InfraLogEntriesDomain { return { columnId: column.fieldColumn.id, field: column.fieldColumn.field, - value: doc.fields[column.fieldColumn.field], - highlights: doc.highlights[column.fieldColumn.field] || [], + value: doc.fields[column.fieldColumn.field] ?? [], + highlights: doc.highlights[column.fieldColumn.field] ?? [], }; } } @@ -252,8 +252,8 @@ export class InfraLogEntriesDomain { ): Promise { const document = await this.adapter.getLogItem(requestContext, id, sourceConfiguration); const defaultFields = [ - { field: '_index', value: document._index }, - { field: '_id', value: document._id }, + { field: '_index', value: [document._index] }, + { field: '_id', value: [document._id] }, ]; return { @@ -310,10 +310,10 @@ export class InfraLogEntriesDomain { } } -interface LogItemHit { +export interface LogItemHit { _index: string; _id: string; - fields: { [field: string]: [value: unknown] }; + fields: { [field: string]: [value: JsonArray] }; sort: [number, number]; } @@ -400,9 +400,9 @@ const createHighlightQueryDsl = (phrase: string, fields: string[]) => ({ const getContextFromDoc = (doc: LogEntryDocument): LogEntry['context'] => { // Get all context fields, then test for the presence and type of the ones that go together - const containerId = doc.fields['container.id']; - const hostName = doc.fields['host.name']; - const logFilePath = doc.fields['log.file.path']; + const containerId = doc.fields['container.id']?.[0]; + const hostName = doc.fields['host.name']?.[0]; + const logFilePath = doc.fields['log.file.path']?.[0]; if (typeof containerId === 'string') { return { 'container.id': containerId }; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts index b8cadaa06f61b..f9775e127088a 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import stringify from 'json-stable-stringify'; - -import { InfraLogMessageSegment } from '../../../graphql/types'; +import { LogMessagePart } from '../../../../common/http_api/log_entries'; +import { JsonArray, JsonValue } from '../../../../common/typed_json'; import { LogMessageFormattingCondition, + LogMessageFormattingFieldValueConditionValue, LogMessageFormattingInstruction, LogMessageFormattingRule, } from './rule_types'; @@ -30,7 +30,7 @@ export function compileFormattingRules( ) ) ), - format(fields, highlights): InfraLogMessageSegment[] { + format(fields, highlights): LogMessagePart[] { for (const compiledRule of compiledRules) { if (compiledRule.fulfillsCondition(fields)) { return compiledRule.format(fields, highlights); @@ -59,16 +59,34 @@ const compileRule = (rule: LogMessageFormattingRule): CompiledLogMessageFormatti const compileCondition = ( condition: LogMessageFormattingCondition ): CompiledLogMessageFormattingCondition => - [compileExistsCondition, compileFieldValueCondition].reduce( - (compiledCondition, compile) => compile(condition) || compiledCondition, - catchAllCondition - ); + [ + compileAllCondition, + compileExistsCondition, + compileExistsPrefixCondition, + compileFieldValueCondition, + ].reduce((compiledCondition, compile) => compile(condition) || compiledCondition, falseCondition); -const catchAllCondition: CompiledLogMessageFormattingCondition = { +const falseCondition: CompiledLogMessageFormattingCondition = { conditionFields: [] as string[], fulfillsCondition: () => false, }; +const compileAllCondition = ( + condition: LogMessageFormattingCondition +): CompiledLogMessageFormattingCondition | null => { + if (!('all' in condition)) { + return null; + } + + const compiledConditions = condition.all.map(compileCondition); + + return { + conditionFields: compiledConditions.flatMap(({ conditionFields }) => conditionFields), + fulfillsCondition: (fields: Fields) => + compiledConditions.every(({ fulfillsCondition }) => fulfillsCondition(fields)), + }; +}; + const compileExistsCondition = (condition: LogMessageFormattingCondition) => 'exists' in condition ? { @@ -78,13 +96,24 @@ const compileExistsCondition = (condition: LogMessageFormattingCondition) => } : null; +const compileExistsPrefixCondition = (condition: LogMessageFormattingCondition) => + 'existsPrefix' in condition + ? { + conditionFields: condition.existsPrefix.map((prefix) => `${prefix}.*`), + fulfillsCondition: (fields: Fields) => + condition.existsPrefix.every((fieldNamePrefix) => + Object.keys(fields).some((field) => field.startsWith(`${fieldNamePrefix}.`)) + ), + } + : null; + const compileFieldValueCondition = (condition: LogMessageFormattingCondition) => 'values' in condition ? { conditionFields: Object.keys(condition.values), fulfillsCondition: (fields: Fields) => - Object.entries(condition.values).every( - ([fieldName, expectedValue]) => fields[fieldName] === expectedValue + Object.entries(condition.values).every(([fieldName, expectedValue]) => + equalsOrContains(fields[fieldName] ?? [], expectedValue) ), } : null; @@ -116,7 +145,11 @@ const compileFormattingInstructions = ( const compileFormattingInstruction = ( formattingInstruction: LogMessageFormattingInstruction ): CompiledLogMessageFormattingInstruction => - [compileFieldReferenceFormattingInstruction, compileConstantFormattingInstruction].reduce( + [ + compileFieldReferenceFormattingInstruction, + compileFieldsPrefixReferenceFormattingInstruction, + compileConstantFormattingInstruction, + ].reduce( (compiledFormattingInstruction, compile) => compile(formattingInstruction) || compiledFormattingInstruction, catchAllFormattingInstruction @@ -138,19 +171,44 @@ const compileFieldReferenceFormattingInstruction = ( ? { formattingFields: [formattingInstruction.field], format: (fields, highlights) => { - const value = fields[formattingInstruction.field]; - const highlightedValues = highlights[formattingInstruction.field]; + const value = fields[formattingInstruction.field] ?? []; + const highlightedValues = highlights[formattingInstruction.field] ?? []; return [ { field: formattingInstruction.field, - value: typeof value === 'object' ? stringify(value) : `${value}`, - highlights: highlightedValues || [], + value, + highlights: highlightedValues, }, ]; }, } : null; +const compileFieldsPrefixReferenceFormattingInstruction = ( + formattingInstruction: LogMessageFormattingInstruction +): CompiledLogMessageFormattingInstruction | null => + 'fieldsPrefix' in formattingInstruction + ? { + formattingFields: [`${formattingInstruction.fieldsPrefix}.*`], + format: (fields, highlights) => { + const matchingFields = Object.keys(fields).filter((field) => + field.startsWith(`${formattingInstruction.fieldsPrefix}.`) + ); + return matchingFields.flatMap((field) => { + const value = fields[field] ?? []; + const highlightedValues = highlights[field] ?? []; + return [ + { + field, + value, + highlights: highlightedValues, + }, + ]; + }); + }, + } + : null; + const compileConstantFormattingInstruction = ( formattingInstruction: LogMessageFormattingInstruction ): CompiledLogMessageFormattingInstruction | null => @@ -165,8 +223,21 @@ const compileConstantFormattingInstruction = ( } : null; +const equalsOrContains = ( + operand: JsonValue, + value: LogMessageFormattingFieldValueConditionValue +): boolean => { + if (Array.isArray(operand)) { + return operand.includes(value); + } else if (typeof operand === 'object' && operand !== null) { + return Object.values(operand).includes(value); + } else { + return operand === value; + } +}; + export interface Fields { - [fieldName: string]: string | number | object | boolean | null; + [fieldName: string]: JsonArray; } export interface Highlights { @@ -176,7 +247,7 @@ export interface Highlights { export interface CompiledLogMessageFormattingRule { requiredFields: string[]; fulfillsCondition(fields: Fields): boolean; - format(fields: Fields, highlights: Highlights): InfraLogMessageSegment[]; + format(fields: Fields, highlights: Highlights): LogMessagePart[]; } export interface CompiledLogMessageFormattingCondition { @@ -186,5 +257,5 @@ export interface CompiledLogMessageFormattingCondition { export interface CompiledLogMessageFormattingInstruction { formattingFields: string[]; - format(fields: Fields, highlights: Highlights): InfraLogMessageSegment[]; + format(fields: Fields, highlights: Highlights): LogMessagePart[]; } diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/rule_types.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/rule_types.ts index 6107fc362f8e3..4569f4b8e8a91 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/rule_types.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/rule_types.ts @@ -4,33 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ +import { JsonValue } from '../../../../common/typed_json'; + export interface LogMessageFormattingRule { when: LogMessageFormattingCondition; format: LogMessageFormattingInstruction[]; } export type LogMessageFormattingCondition = + | LogMessageFormattingAllCondition | LogMessageFormattingExistsCondition + | LogMessageFormattingExistsPrefixCondition | LogMessageFormattingFieldValueCondition; +export interface LogMessageFormattingAllCondition { + all: LogMessageFormattingCondition[]; +} + export interface LogMessageFormattingExistsCondition { exists: string[]; } +export interface LogMessageFormattingExistsPrefixCondition { + existsPrefix: string[]; +} + export interface LogMessageFormattingFieldValueCondition { values: { - [fieldName: string]: string | number | boolean | null; + [fieldName: string]: LogMessageFormattingFieldValueConditionValue; }; } +export type LogMessageFormattingFieldValueConditionValue = JsonValue; + export type LogMessageFormattingInstruction = | LogMessageFormattingFieldReference + | LogMessageFormattingFieldsPrefixReference | LogMessageFormattingConstant; export interface LogMessageFormattingFieldReference { field: string; } +export interface LogMessageFormattingFieldsPrefixReference { + fieldsPrefix: string; +} + export interface LogMessageFormattingConstant { constant: string; } diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts index 183254a0486a2..9401d34ca62fc 100644 --- a/x-pack/plugins/infra/server/lib/metrics/index.ts +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -17,11 +17,20 @@ import { EMPTY_RESPONSE } from './constants'; import { createAggregations } from './lib/create_aggregations'; import { convertHistogramBucketsToTimeseries } from './lib/convert_histogram_buckets_to_timeseries'; import { calculateBucketSize } from './lib/calculate_bucket_size'; +import { calculatedInterval } from './lib/calculate_interval'; export const query = async ( search: ESSearchClient, - options: MetricsAPIRequest + rawOptions: MetricsAPIRequest ): Promise => { + const interval = await calculatedInterval(search, rawOptions); + const options = { + ...rawOptions, + timerange: { + ...rawOptions.timerange, + interval, + }, + }; const hasGroupBy = Array.isArray(options.groupBy) && options.groupBy.length > 0; const filter: Array> = [ { @@ -35,6 +44,7 @@ export const query = async ( }, ...(options.groupBy?.map((field) => ({ exists: { field } })) ?? []), ]; + const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -70,7 +80,7 @@ export const query = async ( throw new Error('Aggregations should be present.'); } - const { bucketSize } = calculateBucketSize(options.timerange); + const { bucketSize } = calculateBucketSize({ ...options.timerange, interval }); if (hasGroupBy && GroupingResponseRT.is(response.aggregations)) { const { groupings } = response.aggregations; diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts new file mode 100644 index 0000000000000..46682e2213a3c --- /dev/null +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isArray, isNumber } from 'lodash'; +import { MetricsAPIRequest } from '../../../../common/http_api'; +import { ESSearchClient } from '../types'; +import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; + +export const calculatedInterval = async (search: ESSearchClient, options: MetricsAPIRequest) => { + const useModuleInterval = + options.timerange.interval === 'modules' && + isArray(options.modules) && + options.modules.length > 0; + + const calcualatedInterval = useModuleInterval + ? await calculateMetricInterval( + search, + { + indexPattern: options.indexPattern, + timestampField: options.timerange.field, + timerange: { from: options.timerange.from, to: options.timerange.to }, + }, + options.modules + ) + : false; + + const defaultInterval = + options.timerange.interval === 'modules' ? 'auto' : options.timerange.interval; + + return isNumber(calcualatedInterval) ? `>=${calcualatedInterval}s` : defaultInterval; +}; diff --git a/x-pack/plugins/infra/server/routes/metrics_api/index.ts b/x-pack/plugins/infra/server/routes/metrics_api/index.ts new file mode 100644 index 0000000000000..f3dcdeeb70cc1 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/metrics_api/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { schema } from '@kbn/config-schema'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { throwErrors } from '../../../common/runtime_types'; +import { createSearchClient } from '../../lib/create_search_client'; +import { query } from '../../lib/metrics'; +import { MetricsAPIRequestRT, MetricsAPIResponseRT } from '../../../common/http_api'; + +const escapeHatch = schema.object({}, { unknowns: 'allow' }); + +export const initMetricsAPIRoute = (libs: InfraBackendLibs) => { + const { framework } = libs; + framework.registerRoute( + { + method: 'post', + path: '/api/infra/metrics_api', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { + try { + const options = pipe( + MetricsAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const client = createSearchClient(requestContext, framework); + const metricsApiResponse = await query(client, options); + + return response.ok({ + body: MetricsAPIResponseRT.encode(metricsApiResponse), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/ingest_manager/common/services/is_valid_namespace.ts b/x-pack/plugins/ingest_manager/common/services/is_valid_namespace.ts index 43aa64f677263..8bd8349580edc 100644 --- a/x-pack/plugins/ingest_manager/common/services/is_valid_namespace.ts +++ b/x-pack/plugins/ingest_manager/common/services/is_valid_namespace.ts @@ -12,26 +12,23 @@ export function isValidNamespace(namespace: string): { valid: boolean; error?: s if (!namespace.trim()) { return { valid: false, - error: i18n.translate('xpack.ingestManager.namespaceValidation.requiredErrorMessage', { + error: i18n.translate('xpack.fleet.namespaceValidation.requiredErrorMessage', { defaultMessage: 'Namespace is required', }), }; } else if (namespace !== namespace.toLowerCase()) { return { valid: false, - error: i18n.translate('xpack.ingestManager.namespaceValidation.lowercaseErrorMessage', { + error: i18n.translate('xpack.fleet.namespaceValidation.lowercaseErrorMessage', { defaultMessage: 'Namespace must be lowercase', }), }; } else if (/[\*\\/\?"<>|\s,#:]+/.test(namespace)) { return { valid: false, - error: i18n.translate( - 'xpack.ingestManager.namespaceValidation.invalidCharactersErrorMessage', - { - defaultMessage: 'Namespace contains invalid characters', - } - ), + error: i18n.translate('xpack.fleet.namespaceValidation.invalidCharactersErrorMessage', { + defaultMessage: 'Namespace contains invalid characters', + }), }; } // Node.js doesn't have Blob, and browser doesn't have Buffer :) @@ -41,7 +38,7 @@ export function isValidNamespace(namespace: string): { valid: boolean; error?: s ) { return { valid: false, - error: i18n.translate('xpack.ingestManager.namespaceValidation.tooLongErrorMessage', { + error: i18n.translate('xpack.fleet.namespaceValidation.tooLongErrorMessage', { defaultMessage: 'Namespace cannot be more than 100 bytes', }), }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx index e321dfb1826f7..a6a162dbdeafa 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx @@ -27,7 +27,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => {

    @@ -37,13 +37,13 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => {

    = ({ onClose }) => { target="_blank" > @@ -61,7 +61,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { forumLink: ( @@ -74,7 +74,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx index 62158483518dd..e39d07eec395e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx @@ -26,20 +26,17 @@ export const AlphaMessaging: React.FC<{}> = () => {

    - + {' – '} {' '} setIsAlphaFlyoutOpen(true)}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/context_menu_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/context_menu_actions.tsx index 7d1f12447340f..29e42363e9865 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/context_menu_actions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/context_menu_actions.tsx @@ -63,7 +63,7 @@ export const ContextMenuActions = React.memo(({ button, onChange, isOpen, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx index a24640af7438f..343ddfab23c71 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/enrollment_instructions/manual/index.tsx @@ -38,7 +38,7 @@ export const ManualInstructions: React.FunctionComponent = ({ <> @@ -46,7 +46,7 @@ export const ManualInstructions: React.FunctionComponent = ({

    @@ -59,7 +59,7 @@ export const ManualInstructions: React.FunctionComponent = ({

    @@ -71,7 +71,7 @@ export const ManualInstructions: React.FunctionComponent = ({ = ({ href="https://www.elastic.co/guide/en/ingest-management/current/elastic-agent-installation-configuration.html" > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_header_link.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_header_link.tsx index 2fd99a88c3c89..88759791a5300 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_header_link.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_header_link.tsx @@ -33,7 +33,7 @@ const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(( return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx index 64dbb3f43312c..87fd58861b274 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx @@ -60,13 +60,13 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => { iconType="cheer" title={ @@ -77,7 +77,7 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => { >

    { blogPostLink: ( @@ -98,7 +98,7 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => {

    @@ -113,7 +113,7 @@ const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => { }} > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx index 7fec1909ba22b..3430a4eb5b258 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx @@ -26,14 +26,14 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }

    @@ -45,7 +45,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName } })} > @@ -57,7 +57,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName } target="_blank" > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx index 9069cc0f73806..24a5b7e4c2bc0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx @@ -61,7 +61,7 @@ export const SearchBar: React.FunctionComponent = ({ icon={'search'} placeholder={ placeholder || - i18n.translate('xpack.ingestManager.defaultSearchPlaceholderText', { + i18n.translate('xpack.fleet.defaultSearchPlaceholderText', { defaultMessage: 'Search', }) } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx index 7f80863faee45..80ecaa2493278 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx @@ -41,21 +41,21 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { const kibanaUrlsInput = useComboInput([], (value) => { if (value.length === 0) { return [ - i18n.translate('xpack.ingestManager.settings.kibanaUrlEmptyError', { + i18n.translate('xpack.fleet.settings.kibanaUrlEmptyError', { defaultMessage: 'At least one URL is required', }), ]; } if (value.some((v) => !v.match(URL_REGEX))) { return [ - i18n.translate('xpack.ingestManager.settings.kibanaUrlError', { + i18n.translate('xpack.fleet.settings.kibanaUrlError', { defaultMessage: 'Invalid URL', }), ]; } if (isDiffPathProtocol(value)) { return [ - i18n.translate('xpack.ingestManager.settings.kibanaUrlDifferentPathOrProtocolError', { + i18n.translate('xpack.fleet.settings.kibanaUrlDifferentPathOrProtocolError', { defaultMessage: 'Protocol and path must be the same for each URL', }), ]; @@ -64,7 +64,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { const elasticsearchUrlInput = useComboInput([], (value) => { if (value.some((v) => !v.match(URL_REGEX))) { return [ - i18n.translate('xpack.ingestManager.settings.elasticHostError', { + i18n.translate('xpack.fleet.settings.elasticHostError', { defaultMessage: 'Invalid URL', }), ]; @@ -77,7 +77,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { return; } catch (error) { return [ - i18n.translate('xpack.ingestManager.settings.invalidYamlFormatErrorMessage', { + i18n.translate('xpack.fleet.settings.invalidYamlFormatErrorMessage', { defaultMessage: 'Invalid YAML: {reason}', values: { reason: error.message }, }), @@ -114,7 +114,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { throw settingsResponse.error; } notifications.toasts.addSuccess( - i18n.translate('xpack.ingestManager.settings.success.message', { + i18n.translate('xpack.fleet.settings.success.message', { defaultMessage: 'Settings saved', }) ); @@ -166,7 +166,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { options={[ { id: 'enabled', - label: i18n.translate('xpack.ingestManager.settings.autoUpgradeEnabledLabel', { + label: i18n.translate('xpack.fleet.settings.autoUpgradeEnabledLabel', { defaultMessage: 'Automatically update agent binaries to use the latest minor version.', }), @@ -174,7 +174,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { { id: 'disabled', disabled: true, - label: i18n.translate('xpack.ingestManager.settings.autoUpgradeDisabledLabel', { + label: i18n.translate('xpack.fleet.settings.autoUpgradeDisabledLabel', { defaultMessage: 'Manually manage agent binary versions. Requires a Gold subscription.', }), @@ -187,7 +187,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {

    @@ -200,23 +200,17 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { options={[ { id: 'enabled', - label: i18n.translate( - 'xpack.ingestManager.settings.integrationUpgradeEnabledFieldLabel', - { - defaultMessage: - 'Automatically update integrations to the latest version to get the latest assets. You might need to update agent policies to use new features.', - } - ), + label: i18n.translate('xpack.fleet.settings.integrationUpgradeEnabledFieldLabel', { + defaultMessage: + 'Automatically update integrations to the latest version to get the latest assets. You might need to update agent policies to use new features.', + }), }, { id: 'disabled', disabled: true, - label: i18n.translate( - 'xpack.ingestManager.settings.integrationUpgradeDisabledFieldLabel', - { - defaultMessage: 'Manually manage integration versions yourself.', - } - ), + label: i18n.translate('xpack.fleet.settings.integrationUpgradeDisabledFieldLabel', { + defaultMessage: 'Manually manage integration versions yourself.', + }), }, ]} idSelected={'enabled'} @@ -226,7 +220,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {

    @@ -238,7 +232,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {

    @@ -246,14 +240,14 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { = ({ onClose }) => { = ({ onClose }) => { = ({ onClose }) => {

    @@ -317,7 +311,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { @@ -325,7 +319,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index ccd889dec5b9f..ed38e1a5ce4a1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -10,7 +10,7 @@ import { useCore } from './use_core'; const BASE_BREADCRUMB: ChromeBreadcrumb = { href: pagePathGetters.overview(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.appTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.appTitle', { defaultMessage: 'Fleet', }), }; @@ -22,7 +22,7 @@ const breadcrumbGetters: { overview: () => [ BASE_BREADCRUMB, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.overviewPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.overviewPageTitle', { defaultMessage: 'Overview', }), }, @@ -30,7 +30,7 @@ const breadcrumbGetters: { integrations: () => [ BASE_BREADCRUMB, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.integrationsPageTitle', { defaultMessage: 'Integrations', }), }, @@ -39,12 +39,12 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.integrations(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.integrationsPageTitle', { defaultMessage: 'Integrations', }), }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.allIntegrationsPageTitle', { defaultMessage: 'All', }), }, @@ -53,12 +53,12 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.integrations(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.integrationsPageTitle', { defaultMessage: 'Integrations', }), }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.installedIntegrationsPageTitle', { defaultMessage: 'Installed', }), }, @@ -67,7 +67,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.integrations(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.integrationsPageTitle', { defaultMessage: 'Integrations', }), }, @@ -76,7 +76,7 @@ const breadcrumbGetters: { policies: () => [ BASE_BREADCRUMB, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.policiesPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', { defaultMessage: 'Policies', }), }, @@ -84,7 +84,7 @@ const breadcrumbGetters: { policies_list: () => [ BASE_BREADCRUMB, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.policiesPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', { defaultMessage: 'Policies', }), }, @@ -93,7 +93,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.policies(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.policiesPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', { defaultMessage: 'Policies', }), }, @@ -103,7 +103,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.policies(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.policiesPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', { defaultMessage: 'Policies', }), }, @@ -112,7 +112,7 @@ const breadcrumbGetters: { text: policyName, }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.addPackagePolicyPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.addPackagePolicyPageTitle', { defaultMessage: 'Add integration', }), }, @@ -121,7 +121,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.integrations(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.integrationsPageTitle', { defaultMessage: 'Integrations', }), }, @@ -130,7 +130,7 @@ const breadcrumbGetters: { text: pkgTitle, }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.addPackagePolicyPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.addPackagePolicyPageTitle', { defaultMessage: 'Add integration', }), }, @@ -139,7 +139,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.policies(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.policiesPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', { defaultMessage: 'Policies', }), }, @@ -148,7 +148,7 @@ const breadcrumbGetters: { text: policyName, }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.editPackagePolicyPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.editPackagePolicyPageTitle', { defaultMessage: 'Edit integration', }), }, @@ -156,7 +156,7 @@ const breadcrumbGetters: { fleet: () => [ BASE_BREADCRUMB, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.agentsPageTitle', { defaultMessage: 'Agents', }), }, @@ -164,7 +164,7 @@ const breadcrumbGetters: { fleet_agent_list: () => [ BASE_BREADCRUMB, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.agentsPageTitle', { defaultMessage: 'Agents', }), }, @@ -173,7 +173,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.fleet(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.agentsPageTitle', { defaultMessage: 'Agents', }), }, @@ -183,12 +183,12 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { href: pagePathGetters.fleet(), - text: i18n.translate('xpack.ingestManager.breadcrumbs.agentsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.agentsPageTitle', { defaultMessage: 'Agents', }), }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.enrollmentTokensPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.enrollmentTokensPageTitle', { defaultMessage: 'Enrollment tokens', }), }, @@ -196,7 +196,7 @@ const breadcrumbGetters: { data_streams: () => [ BASE_BREADCRUMB, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.datastreamsPageTitle', { + text: i18n.translate('xpack.fleet.breadcrumbs.datastreamsPageTitle', { defaultMessage: 'Data streams', }), }, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 12d6e1c9ed0b4..34c5742ee96a3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -115,16 +115,13 @@ const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basep } - error={i18n.translate( - 'xpack.ingestManager.permissionsRequestErrorMessageDescription', - { - defaultMessage: 'There was a problem checking Fleet permissions', - } - )} + error={i18n.translate('xpack.fleet.permissionsRequestErrorMessageDescription', { + defaultMessage: 'There was a problem checking Fleet permissions', + })} /> ) : ( @@ -134,12 +131,12 @@ const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basep

    {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( ) : ( )} @@ -149,13 +146,13 @@ const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basep

    {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( superuser }} /> ) : ( )} @@ -175,7 +172,7 @@ const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basep } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 5d39995922b93..376de7e2e6a07 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -63,19 +63,19 @@ export const DefaultLayout: React.FunctionComponent = ({ @@ -85,13 +85,13 @@ export const DefaultLayout: React.FunctionComponent = ({ disabled={!agents?.enabled} > @@ -106,7 +106,7 @@ export const DefaultLayout: React.FunctionComponent = ({ target="_blank" > @@ -115,7 +115,7 @@ export const DefaultLayout: React.FunctionComponent = ({ setIsSettingsFlyoutOpen(true)}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/actions_menu.tsx index 691de5f259134..55872aee5c3d3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/actions_menu.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/actions_menu.tsx @@ -69,7 +69,7 @@ export const AgentPolicyActionMenu = memo<{ }, children: ( ), @@ -84,7 +84,7 @@ export const AgentPolicyActionMenu = memo<{ key="enrollAgents" > , @@ -94,7 +94,7 @@ export const AgentPolicyActionMenu = memo<{ key="viewPolicy" > , @@ -107,7 +107,7 @@ export const AgentPolicyActionMenu = memo<{ key="copyPolicy" > , diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx index fefbe1fa82a0c..41201f9612f13 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx @@ -37,13 +37,10 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr setIsModalOpen(true); setAgentPolicy(agentPolicyToCopy); setNewAgentPolicy({ - name: i18n.translate( - 'xpack.ingestManager.copyAgentPolicy.confirmModal.defaultNewPolicyName', - { - defaultMessage: '{name} (copy)', - values: { name: agentPolicyToCopy.name }, - } - ), + name: i18n.translate('xpack.fleet.copyAgentPolicy.confirmModal.defaultNewPolicyName', { + defaultMessage: '{name} (copy)', + values: { name: agentPolicyToCopy.name }, + }), description: agentPolicyToCopy.description, }); onSuccessCallback.current = onSuccess; @@ -63,7 +60,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr if (data) { notifications.toasts.addSuccess( - i18n.translate('xpack.ingestManager.copyAgentPolicy.successNotificationTitle', { + i18n.translate('xpack.fleet.copyAgentPolicy.successNotificationTitle', { defaultMessage: 'Agent policy copied', }) ); @@ -72,7 +69,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr } } else { notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.copyAgentPolicy.failureNotificationTitle', { + i18n.translate('xpack.fleet.copyAgentPolicy.failureNotificationTitle', { defaultMessage: "Error copying agent policy '{id}'", values: { id: agentPolicy!.id }, }) @@ -80,7 +77,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr } } catch (e) { notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.copyAgentPolicy.fatalErrorNotificationTitle', { + i18n.translate('xpack.fleet.copyAgentPolicy.fatalErrorNotificationTitle', { defaultMessage: 'Error copying agent policy', }) ); @@ -99,7 +96,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr title={ = ({ childr onConfirm={copyAgentPolicy} cancelButtonText={ } confirmButtonText={ } @@ -125,14 +122,14 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr >

    } @@ -147,7 +144,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx index ab8a9c99227c5..41704f69958a0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -61,7 +61,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil if (data) { notifications.toasts.addSuccess( - i18n.translate('xpack.ingestManager.deleteAgentPolicy.successSingleNotificationTitle', { + i18n.translate('xpack.fleet.deleteAgentPolicy.successSingleNotificationTitle', { defaultMessage: "Deleted agent policy '{id}'", values: { id: data.name || data.id }, }) @@ -71,7 +71,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil } } else { notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.deleteAgentPolicy.failureSingleNotificationTitle', { + i18n.translate('xpack.fleet.deleteAgentPolicy.failureSingleNotificationTitle', { defaultMessage: "Error deleting agent policy '{id}'", values: { id: agentPolicy }, }) @@ -79,7 +79,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil } } catch (e) { notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.deleteAgentPolicy.fatalErrorNotificationTitle', { + i18n.translate('xpack.fleet.deleteAgentPolicy.fatalErrorNotificationTitle', { defaultMessage: 'Error deleting agent policy', }) ); @@ -113,7 +113,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil } @@ -121,19 +121,19 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil onConfirm={deleteAgentPolicy} cancelButtonText={ } confirmButtonText={ isLoading || isLoadingAgentsCount ? ( ) : ( ) @@ -143,21 +143,21 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil > {isLoadingAgentsCount ? ( ) : agentsCount ? ( = ({ chil ) : ( )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_form.tsx index c716f7b12e78c..919b6d3669a6b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_form.tsx @@ -46,7 +46,7 @@ export const agentPolicyFormValidation = ( if (!agentPolicy.name?.trim()) { errors.name = [ , ]; @@ -88,12 +88,9 @@ export const AgentPolicyForm: React.FunctionComponent = ({ { name: 'name', label: ( - + ), - placeholder: i18n.translate('xpack.ingestManager.agentPolicyForm.nameFieldPlaceholder', { + placeholder: i18n.translate('xpack.fleet.agentPolicyForm.nameFieldPlaceholder', { defaultMessage: 'Choose a name', }), }, @@ -101,16 +98,13 @@ export const AgentPolicyForm: React.FunctionComponent = ({ name: 'description', label: ( ), - placeholder: i18n.translate( - 'xpack.ingestManager.agentPolicyForm.descriptionFieldPlaceholder', - { - defaultMessage: 'How will this policy be used?', - } - ), + placeholder: i18n.translate('xpack.fleet.agentPolicyForm.descriptionFieldPlaceholder', { + defaultMessage: 'How will this policy be used?', + }), }, ]; }, []); @@ -120,14 +114,14 @@ export const AgentPolicyForm: React.FunctionComponent = ({ title={

    } description={ } @@ -163,14 +157,14 @@ export const AgentPolicyForm: React.FunctionComponent = ({ title={

    } description={ } @@ -202,14 +196,14 @@ export const AgentPolicyForm: React.FunctionComponent = ({ title={

    } description={ } @@ -221,12 +215,12 @@ export const AgentPolicyForm: React.FunctionComponent = ({ label: ( <> {' '} = ({ label: ( <> {' '} = ({ title={

    @@ -298,7 +292,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ description={ <> @@ -311,7 +305,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ onClick={() => deleteAgentPolicyPrompt(agentPolicy.id!, onDelete)} > @@ -323,7 +317,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ @@ -343,7 +337,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ } @@ -353,12 +347,12 @@ export const AgentPolicyForm: React.FunctionComponent = ({ label={ <> {' '} = ({ id="advancedOptions" buttonContent={ } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx index fefb427df5ea6..773d53484147a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx @@ -41,7 +41,7 @@ export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => voi } @@ -69,13 +69,13 @@ export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => voi

    {agentPolicyData?.item ? ( ) : ( )} @@ -88,7 +88,7 @@ export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => voi @@ -100,7 +100,7 @@ export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => voi isDisabled={Boolean(isLoadingYaml && !yamlData)} > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/confirm_deploy_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/confirm_deploy_modal.tsx index d1a9b4812bfe0..9b88ee24651ba 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/confirm_deploy_modal.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/confirm_deploy_modal.tsx @@ -21,7 +21,7 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{ } @@ -29,13 +29,13 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{ onConfirm={onConfirm} cancelButtonText={ } confirmButtonText={ } @@ -43,7 +43,7 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{ >
    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/linked_agent_count.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/linked_agent_count.tsx index 72e60ae678739..c602f492f74c6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/linked_agent_count.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/linked_agent_count.tsx @@ -15,7 +15,7 @@ export const LinkedAgentCount = memo<{ count: number; agentPolicyId: string }>( const { getHref } = useLink(); const displayValue = ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx index ff5961b94f726..8de40edc40331 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/package_policy_delete_provider.tsx @@ -95,40 +95,28 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ if (successfulResults.length) { const hasMultipleSuccesses = successfulResults.length > 1; const successMessage = hasMultipleSuccesses - ? i18n.translate( - 'xpack.ingestManager.deletePackagePolicy.successMultipleNotificationTitle', - { - defaultMessage: 'Deleted {count} integrations', - values: { count: successfulResults.length }, - } - ) - : i18n.translate( - 'xpack.ingestManager.deletePackagePolicy.successSingleNotificationTitle', - { - defaultMessage: "Deleted integration '{id}'", - values: { id: successfulResults[0].name || successfulResults[0].id }, - } - ); + ? i18n.translate('xpack.fleet.deletePackagePolicy.successMultipleNotificationTitle', { + defaultMessage: 'Deleted {count} integrations', + values: { count: successfulResults.length }, + }) + : i18n.translate('xpack.fleet.deletePackagePolicy.successSingleNotificationTitle', { + defaultMessage: "Deleted integration '{id}'", + values: { id: successfulResults[0].name || successfulResults[0].id }, + }); notifications.toasts.addSuccess(successMessage); } if (failedResults.length) { const hasMultipleFailures = failedResults.length > 1; const failureMessage = hasMultipleFailures - ? i18n.translate( - 'xpack.ingestManager.deletePackagePolicy.failureMultipleNotificationTitle', - { - defaultMessage: 'Error deleting {count} integrations', - values: { count: failedResults.length }, - } - ) - : i18n.translate( - 'xpack.ingestManager.deletePackagePolicy.failureSingleNotificationTitle', - { - defaultMessage: "Error deleting integration '{id}'", - values: { id: failedResults[0].id }, - } - ); + ? i18n.translate('xpack.fleet.deletePackagePolicy.failureMultipleNotificationTitle', { + defaultMessage: 'Error deleting {count} integrations', + values: { count: failedResults.length }, + }) + : i18n.translate('xpack.fleet.deletePackagePolicy.failureSingleNotificationTitle', { + defaultMessage: "Error deleting integration '{id}'", + values: { id: failedResults[0].id }, + }); notifications.toasts.addDanger(failureMessage); } @@ -137,7 +125,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ } } catch (e) { notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.deletePackagePolicy.fatalErrorNotificationTitle', { + i18n.translate('xpack.fleet.deletePackagePolicy.fatalErrorNotificationTitle', { defaultMessage: 'Error deleting integration', }) ); @@ -157,7 +145,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ @@ -166,19 +154,19 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ onConfirm={deletePackagePolicies} cancelButtonText={ } confirmButtonText={ isLoading || isLoadingAgentsCount ? ( ) : ( = ({ > {isLoadingAgentsCount ? ( ) : agentsCount ? ( @@ -200,14 +188,14 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ color="danger" title={ } > {agentPolicy.name}, @@ -219,7 +207,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ ) : null} {!isLoadingAgentsCount && ( )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx index 47c14ce0a3e81..d5163e1b9abbe 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx @@ -46,7 +46,7 @@ const EmptyPackagePolicy: CustomConfigurePackagePolicyContent = () => (

    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx index 6edce74d162bb..9188f0069b8bf 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -54,7 +54,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{

    {from === 'edit' ? ( ) : (

    @@ -89,7 +89,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{

    @@ -100,17 +100,17 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ const pageDescription = useMemo(() => { return from === 'edit' ? ( ) : from === 'policy' ? ( ) : ( ); @@ -129,7 +129,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ data-test-subj={`${dataTestSubj}_cancelBackLink`} > @@ -149,7 +149,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx index e966ca30c4c97..175bfb1469902 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx @@ -76,7 +76,7 @@ export const PackagePolicyInputConfig: React.FunctionComponent<{

    @@ -85,7 +85,7 @@ export const PackagePolicyInputConfig: React.FunctionComponent<{

    @@ -133,7 +133,7 @@ export const PackagePolicyInputConfig: React.FunctionComponent<{ flush="left" > @@ -142,7 +142,7 @@ export const PackagePolicyInputConfig: React.FunctionComponent<{ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_panel.tsx index 13504a1ed355a..1e43cc0d5938e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_panel.tsx @@ -133,7 +133,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ @@ -148,7 +148,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ aria-label={ isShowingStreams ? i18n.translate( - 'xpack.ingestManager.createPackagePolicy.stepConfigure.hideStreamsAriaLabel', + 'xpack.fleet.createPackagePolicy.stepConfigure.hideStreamsAriaLabel', { defaultMessage: 'Hide {type} inputs', values: { @@ -157,7 +157,7 @@ export const PackagePolicyInputPanel: React.FunctionComponent<{ } ) : i18n.translate( - 'xpack.ingestManager.createPackagePolicy.stepConfigure.showStreamsAriaLabel', + 'xpack.fleet.createPackagePolicy.stepConfigure.showStreamsAriaLabel', { defaultMessage: 'Show {type} inputs', values: { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx index cbc23f4079d9b..3d33edd468151 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx @@ -135,7 +135,7 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{ flush="left" > @@ -144,7 +144,7 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx index 3620b11948b54..9d036f5154b8f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx @@ -78,7 +78,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ !required ? ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx index 70e11d46aa7f6..b45794b9f87db 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx @@ -241,7 +241,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { } notifications.toasts.addSuccess({ - title: i18n.translate('xpack.ingestManager.createPackagePolicy.addedNotificationTitle', { + title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', { defaultMessage: `'{packagePolicyName}' integration added.`, values: { packagePolicyName: packagePolicy.name, @@ -249,7 +249,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { }), text: agentCount && agentPolicy - ? i18n.translate('xpack.ingestManager.createPackagePolicy.addedNotificationMessage', { + ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', { defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`, values: { agentPolicyName: agentPolicy.name, @@ -338,27 +338,21 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { const steps: EuiStepProps[] = [ from === 'package' ? { - title: i18n.translate( - 'xpack.ingestManager.createPackagePolicy.stepSelectAgentPolicyTitle', - { - defaultMessage: 'Select an agent policy', - } - ), + title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', { + defaultMessage: 'Select an agent policy', + }), children: stepSelectAgentPolicy, } : { - title: i18n.translate('xpack.ingestManager.createPackagePolicy.stepSelectPackageTitle', { + title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectPackageTitle', { defaultMessage: 'Select an integration', }), children: stepSelectPackage, }, { - title: i18n.translate( - 'xpack.ingestManager.createPackagePolicy.stepConfigurePackagePolicyTitle', - { - defaultMessage: 'Configure integration', - } - ), + title: i18n.translate('xpack.fleet.createPackagePolicy.stepConfigurePackagePolicyTitle', { + defaultMessage: 'Configure integration', + }), status: !packageInfo || !agentPolicy || isLoadingSecondStep ? 'disabled' : undefined, 'data-test-subj': 'dataCollectionSetupStep', children: stepConfigurePackagePolicy, @@ -392,7 +386,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { {!isLoadingSecondStep && agentPolicy && packageInfo && formState === 'INVALID' ? ( ) : null} @@ -408,7 +402,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { data-test-subj="createPackagePolicyCancelButton" > @@ -424,7 +418,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { data-test-subj="createPackagePolicySaveButton" > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts index 04cd21884e8f2..9ce73c0690ccb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts @@ -54,7 +54,7 @@ export const validatePackagePolicy = ( if (!packagePolicy.name.trim()) { validationResults.name = [ - i18n.translate('xpack.ingestManager.packagePolicyValidation.nameRequiredErrorMessage', { + i18n.translate('xpack.fleet.packagePolicyValidation.nameRequiredErrorMessage', { defaultMessage: 'Name is required', }), ]; @@ -183,7 +183,7 @@ export const validatePackagePolicyConfig = ( if (varDef.required) { if (parsedValue === undefined || (typeof parsedValue === 'string' && !parsedValue)) { errors.push( - i18n.translate('xpack.ingestManager.packagePolicyValidation.requiredErrorMessage', { + i18n.translate('xpack.fleet.packagePolicyValidation.requiredErrorMessage', { defaultMessage: '{fieldName} is required', values: { fieldName: varDef.title || varDef.name, @@ -198,12 +198,9 @@ export const validatePackagePolicyConfig = ( parsedValue = safeLoad(value); } catch (e) { errors.push( - i18n.translate( - 'xpack.ingestManager.packagePolicyValidation.invalidYamlFormatErrorMessage', - { - defaultMessage: 'Invalid YAML format', - } - ) + i18n.translate('xpack.fleet.packagePolicyValidation.invalidYamlFormatErrorMessage', { + defaultMessage: 'Invalid YAML format', + }) ); } } @@ -211,7 +208,7 @@ export const validatePackagePolicyConfig = ( if (varDef.multi) { if (parsedValue && !Array.isArray(parsedValue)) { errors.push( - i18n.translate('xpack.ingestManager.packagePolicyValidation.invalidArrayErrorMessage', { + i18n.translate('xpack.fleet.packagePolicyValidation.invalidArrayErrorMessage', { defaultMessage: 'Invalid format', }) ); @@ -221,7 +218,7 @@ export const validatePackagePolicyConfig = ( (!parsedValue || (Array.isArray(parsedValue) && parsedValue.length === 0)) ) { errors.push( - i18n.translate('xpack.ingestManager.packagePolicyValidation.requiredErrorMessage', { + i18n.translate('xpack.fleet.packagePolicyValidation.requiredErrorMessage', { defaultMessage: '{fieldName} is required', values: { fieldName: varDef.title || varDef.name, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 86f69208e0638..1d242848c3e2e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -87,14 +87,14 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ title={

    } description={ } @@ -106,7 +106,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ error={validationResults.name} label={ } @@ -126,14 +126,14 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ } labelAppend={ @@ -162,7 +162,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ flush="left" > @@ -171,7 +171,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ @@ -190,7 +190,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ error={validationResults.namespace} label={ } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx index ccf9e45ebc4fa..525a224146994 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx @@ -171,7 +171,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ } @@ -186,7 +186,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ } @@ -219,7 +219,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ @@ -230,7 +230,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ onClick={() => setIsCreateAgentPolicyFlyoutOpen(true)} > @@ -241,7 +241,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ helpText={ isFleetReady && selectedPolicyId ? ( } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_package.tsx index 704455b21d64a..8c646323c312c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_package.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_package.tsx @@ -103,7 +103,7 @@ export const StepSelectPackage: React.FunctionComponent<{ } @@ -118,7 +118,7 @@ export const StepSelectPackage: React.FunctionComponent<{ } @@ -157,7 +157,7 @@ export const StepSelectPackage: React.FunctionComponent<{ }} searchProps={{ placeholder: i18n.translate( - 'xpack.ingestManager.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder', + 'xpack.fleet.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder', { defaultMessage: 'Search for integrations', } @@ -190,7 +190,7 @@ export const StepSelectPackage: React.FunctionComponent<{ } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx index b84466ee073eb..40682a28515ff 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/no_package_policies.tsx @@ -18,14 +18,14 @@ export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => { title={

    } body={ } @@ -36,7 +36,7 @@ export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => { href={getHref('add_integration_from_policy', { policyId })} > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index af4c2f78f14a2..c7268d581acc0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -84,12 +84,9 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ { field: 'name', sortable: true, - name: i18n.translate( - 'xpack.ingestManager.policyDetails.packagePoliciesTable.nameColumnTitle', - { - defaultMessage: 'Name', - } - ), + name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle', { + defaultMessage: 'Name', + }), render: (value: string) => ( {value} @@ -99,7 +96,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ { field: 'description', name: i18n.translate( - 'xpack.ingestManager.policyDetails.packagePoliciesTable.descriptionColumnTitle', + 'xpack.fleet.policyDetails.packagePoliciesTable.descriptionColumnTitle', { defaultMessage: 'Description', } @@ -114,7 +111,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ field: 'packageTitle', sortable: true, name: i18n.translate( - 'xpack.ingestManager.policyDetails.packagePoliciesTable.packageNameColumnTitle', + 'xpack.fleet.policyDetails.packagePoliciesTable.packageNameColumnTitle', { defaultMessage: 'Integration', } @@ -140,7 +137,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ { field: 'namespace', name: i18n.translate( - 'xpack.ingestManager.policyDetails.packagePoliciesTable.namespaceColumnTitle', + 'xpack.fleet.policyDetails.packagePoliciesTable.namespaceColumnTitle', { defaultMessage: 'Namespace', } @@ -150,12 +147,9 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ }, }, { - name: i18n.translate( - 'xpack.ingestManager.policyDetails.packagePoliciesTable.actionsColumnTitle', - { - defaultMessage: 'Actions', - } - ), + name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.actionsColumnTitle', { + defaultMessage: 'Actions', + }), actions: [ { render: (packagePolicy: InMemoryPackagePolicy) => ( @@ -169,7 +163,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ // key="packagePolicyView" // > // // , @@ -183,14 +177,14 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ key="packagePolicyEdit" > , // FIXME: implement Copy package policy action // {}} key="packagePolicyCopy"> // // , @@ -205,7 +199,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ }} > @@ -243,7 +237,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ href={getHref('add_integration_from_policy', { policyId: agentPolicy.id })} > , diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx index bc0457ffa0d43..fe3955c84dec3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx @@ -71,7 +71,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( }); if (data) { notifications.toasts.addSuccess( - i18n.translate('xpack.ingestManager.editAgentPolicy.successNotificationTitle', { + i18n.translate('xpack.fleet.editAgentPolicy.successNotificationTitle', { defaultMessage: "Successfully updated '{name}' settings", values: { name: agentPolicy.name }, }) @@ -82,14 +82,14 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( notifications.toasts.addDanger( error ? error.message - : i18n.translate('xpack.ingestManager.editAgentPolicy.errorNotificationTitle', { + : i18n.translate('xpack.fleet.editAgentPolicy.errorNotificationTitle', { defaultMessage: 'Unable to update agent policy', }) ); } } catch (e) { notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.editAgentPolicy.errorNotificationTitle', { + i18n.translate('xpack.fleet.editAgentPolicy.errorNotificationTitle', { defaultMessage: 'Unable to update agent policy', }) ); @@ -145,7 +145,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( @@ -160,7 +160,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( }} > @@ -178,12 +178,12 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( > {isLoading ? ( ) : ( )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/index.tsx index 4be3108dd6eff..7528c923f0abd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/index.tsx @@ -74,7 +74,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { size="xs" > @@ -87,7 +87,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { ) : ( (agentPolicy && agentPolicy.name) || ( { {[ { - label: i18n.translate('xpack.ingestManager.policyDetails.summary.revision', { + label: i18n.translate('xpack.fleet.policyDetails.summary.revision', { defaultMessage: 'Revision', }), content: agentPolicy?.revision ?? 0, }, { isDivider: true }, { - label: i18n.translate('xpack.ingestManager.policyDetails.summary.integrations', { + label: i18n.translate('xpack.fleet.policyDetails.summary.integrations', { defaultMessage: 'Integrations', }), content: ( @@ -141,7 +141,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { }, { isDivider: true }, { - label: i18n.translate('xpack.ingestManager.policyDetails.summary.usedBy', { + label: i18n.translate('xpack.fleet.policyDetails.summary.usedBy', { defaultMessage: 'Used by', }), content: ( @@ -153,7 +153,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { }, { isDivider: true }, { - label: i18n.translate('xpack.ingestManager.policyDetails.summary.lastUpdated', { + label: i18n.translate('xpack.fleet.policyDetails.summary.lastUpdated', { defaultMessage: 'Last updated on', }), content: @@ -217,7 +217,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { return [ { id: 'integrations', - name: i18n.translate('xpack.ingestManager.policyDetails.subTabs.packagePoliciesTabText', { + name: i18n.translate('xpack.fleet.policyDetails.subTabs.packagePoliciesTabText', { defaultMessage: 'Integrations', }), href: getHref('policy_details', { policyId, tabId: 'integrations' }), @@ -225,7 +225,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { }, { id: 'settings', - name: i18n.translate('xpack.ingestManager.policyDetails.subTabs.settingsTabText', { + name: i18n.translate('xpack.fleet.policyDetails.subTabs.settingsTabText', { defaultMessage: 'Settings', }), href: getHref('policy_details', { policyId, tabId: 'settings' }), @@ -248,7 +248,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { } @@ -262,11 +262,11 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { } - error={i18n.translate('xpack.ingestManager.policyDetails.policyNotFoundErrorTitle', { + error={i18n.translate('xpack.fleet.policyDetails.policyNotFoundErrorTitle', { defaultMessage: "Policy '{id}' not found", values: { id: policyId, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/edit_package_policy_page/index.tsx index af4ffab12a791..d642619515a57 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/edit_package_policy_page/index.tsx @@ -214,7 +214,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { if (!error) { history.push(getPath('policy_details', { policyId })); notifications.toasts.addSuccess({ - title: i18n.translate('xpack.ingestManager.editPackagePolicy.updatedNotificationTitle', { + title: i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationTitle', { defaultMessage: `Successfully updated '{packagePolicyName}'`, values: { packagePolicyName: packagePolicy.name, @@ -222,7 +222,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { }), text: agentCount && agentPolicy - ? i18n.translate('xpack.ingestManager.editPackagePolicy.updatedNotificationMessage', { + ? i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationMessage', { defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy`, values: { agentPolicyName: agentPolicy.name, @@ -233,14 +233,14 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { } else { if (error.statusCode === 409) { notifications.toasts.addError(error, { - title: i18n.translate('xpack.ingestManager.editPackagePolicy.failedNotificationTitle', { + title: i18n.translate('xpack.fleet.editPackagePolicy.failedNotificationTitle', { defaultMessage: `Error updating '{packagePolicyName}'`, values: { packagePolicyName: packagePolicy.name, }, }), toastMessage: i18n.translate( - 'xpack.ingestManager.editPackagePolicy.failedConflictNotificationMessage', + 'xpack.fleet.editPackagePolicy.failedConflictNotificationMessage', { defaultMessage: `Data is out of date. Refresh the page to get the latest policy.`, } @@ -248,7 +248,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { }); } else { notifications.toasts.addError(error, { - title: i18n.translate('xpack.ingestManager.editPackagePolicy.failedNotificationTitle', { + title: i18n.translate('xpack.fleet.editPackagePolicy.failedNotificationTitle', { defaultMessage: `Error updating '{packagePolicyName}'`, values: { packagePolicyName: packagePolicy.name, @@ -309,13 +309,13 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { } error={ loadingError || - i18n.translate('xpack.ingestManager.editPackagePolicy.errorLoadingDataMessage', { + i18n.translate('xpack.fleet.editPackagePolicy.errorLoadingDataMessage', { defaultMessage: 'There was an error loading this intergration information', }) } @@ -338,7 +338,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { {agentPolicy && packageInfo && formState === 'INVALID' ? ( ) : null} @@ -348,7 +348,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { @@ -363,7 +363,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { fill > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx index e3e2975777e17..d2c3fc64aa9e6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx @@ -66,7 +66,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({

    @@ -75,7 +75,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({

    @@ -101,7 +101,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ onClose()} flush="left"> @@ -118,31 +118,25 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ setIsLoading(false); if (data) { notifications.toasts.addSuccess( - i18n.translate( - 'xpack.ingestManager.createAgentPolicy.successNotificationTitle', - { - defaultMessage: "Agent policy '{name}' created", - values: { name: agentPolicy.name }, - } - ) + i18n.translate('xpack.fleet.createAgentPolicy.successNotificationTitle', { + defaultMessage: "Agent policy '{name}' created", + values: { name: agentPolicy.name }, + }) ); onClose(data.item); } else { notifications.toasts.addDanger( error ? error.message - : i18n.translate( - 'xpack.ingestManager.createAgentPolicy.errorNotificationTitle', - { - defaultMessage: 'Unable to create agent policy', - } - ) + : i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', { + defaultMessage: 'Unable to create agent policy', + }) ); } } catch (e) { setIsLoading(false); notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.createAgentPolicy.errorNotificationTitle', { + i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', { defaultMessage: 'Unable to create agent policy', }) ); @@ -150,7 +144,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ }} > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/index.tsx index aa4b42986a4f5..8c2fe838bfa43 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/index.tsx @@ -46,7 +46,7 @@ const AgentPolicyListPageLayout: React.FunctionComponent = ({ children }) => (

    @@ -56,7 +56,7 @@ const AgentPolicyListPageLayout: React.FunctionComponent = ({ children }) => (

    @@ -124,7 +124,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { { field: 'name', sortable: true, - name: i18n.translate('xpack.ingestManager.agentPolicyList.nameColumnTitle', { + name: i18n.translate('xpack.fleet.agentPolicyList.nameColumnTitle', { defaultMessage: 'Name', }), width: '20%', @@ -142,7 +142,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { @@ -153,7 +153,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { }, { field: 'description', - name: i18n.translate('xpack.ingestManager.agentPolicyList.descriptionColumnTitle', { + name: i18n.translate('xpack.fleet.agentPolicyList.descriptionColumnTitle', { defaultMessage: 'Description', }), width: '35%', @@ -166,7 +166,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { { field: 'updated_at', sortable: true, - name: i18n.translate('xpack.ingestManager.agentPolicyList.updatedOnColumnTitle', { + name: i18n.translate('xpack.fleet.agentPolicyList.updatedOnColumnTitle', { defaultMessage: 'Last updated on', }), render: (date: AgentPolicy['updated_at']) => ( @@ -175,7 +175,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { }, { field: 'agents', - name: i18n.translate('xpack.ingestManager.agentPolicyList.agentsColumnTitle', { + name: i18n.translate('xpack.fleet.agentPolicyList.agentsColumnTitle', { defaultMessage: 'Agents', }), dataType: 'number', @@ -185,18 +185,15 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { }, { field: 'package_policies', - name: i18n.translate( - 'xpack.ingestManager.agentPolicyList.packagePoliciesCountColumnTitle', - { - defaultMessage: 'Integrations', - } - ), + name: i18n.translate('xpack.fleet.agentPolicyList.packagePoliciesCountColumnTitle', { + defaultMessage: 'Integrations', + }), dataType: 'number', render: (packagePolicies: AgentPolicy['package_policies']) => packagePolicies ? packagePolicies.length : 0, }, { - name: i18n.translate('xpack.ingestManager.agentPolicyList.actionsColumnTitle', { + name: i18n.translate('xpack.fleet.agentPolicyList.actionsColumnTitle', { defaultMessage: 'Actions', }), actions: [ @@ -229,7 +226,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { onClick={() => setIsCreateAgentPolicyFlyoutOpen(true)} > @@ -243,7 +240,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { title={

    @@ -291,7 +288,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { resendRequest()}> @@ -306,20 +303,20 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { noItemsMessage={ isLoading ? ( ) : !search.trim() && (agentPolicyData?.total ?? 0) === 0 ? ( emptyPrompt ) : ( setSearch('')}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx index 057970aa1ee92..90c0d848a5079 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx @@ -16,18 +16,18 @@ export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastre const panels = []; const actionNameSingular = ( ); const actionNamePlural = ( ); - const panelTitle = i18n.translate('xpack.ingestManager.dataStreamList.viewDashboardsPanelTitle', { + const panelTitle = i18n.translate('xpack.fleet.dataStreamList.viewDashboardsPanelTitle', { defaultMessage: 'View dashboards', }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index 4e32fa0bbc1b9..533c273681122 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -31,7 +31,7 @@ const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => (

    @@ -41,7 +41,7 @@ const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => (

    @@ -75,21 +75,21 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { field: 'dataset', sortable: true, width: '25%', - name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.datasetColumnTitle', { defaultMessage: 'Dataset', }), }, { field: 'type', sortable: true, - name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.typeColumnTitle', { defaultMessage: 'Type', }), }, { field: 'namespace', sortable: true, - name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.namespaceColumnTitle', { defaultMessage: 'Namespace', }), render: (namespace: string) => { @@ -99,7 +99,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { { field: 'package', sortable: true, - name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.integrationColumnTitle', { defaultMessage: 'Integration', }), render(pkg: DataStream['package'], datastream: DataStream) { @@ -125,7 +125,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { sortable: true, width: '25%', dataType: 'date', - name: i18n.translate('xpack.ingestManager.dataStreamList.lastActivityColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.lastActivityColumnTitle', { defaultMessage: 'Last activity', }), render: (date: DataStream['last_activity']) => { @@ -140,7 +140,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { { field: 'size_in_bytes', sortable: true, - name: i18n.translate('xpack.ingestManager.dataStreamList.sizeColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.sizeColumnTitle', { defaultMessage: 'Size', }), render: (size: DataStream['size_in_bytes']) => { @@ -153,7 +153,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { }, }, { - name: i18n.translate('xpack.ingestManager.dataStreamList.actionsColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.actionsColumnTitle', { defaultMessage: 'Actions', }), actions: [ @@ -172,7 +172,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { title={

    @@ -236,14 +236,14 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { message={ isLoading ? ( ) : dataStreamsData && !dataStreamsData.data_streams.length ? ( emptyPrompt ) : ( ) @@ -265,25 +265,22 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { onClick={() => resendRequest()} > , ], box: { - placeholder: i18n.translate( - 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle', - { - defaultMessage: 'Filter data streams', - } - ), + placeholder: i18n.translate('xpack.fleet.dataStreamList.searchPlaceholderTitle', { + defaultMessage: 'Filter data streams', + }), incremental: true, }, filters: [ { type: 'field_value_selection', field: 'dataset', - name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.datasetColumnTitle', { defaultMessage: 'Dataset', }), multiSelect: 'or', @@ -293,7 +290,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { { type: 'field_value_selection', field: 'type', - name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.typeColumnTitle', { defaultMessage: 'Type', }), multiSelect: 'or', @@ -303,7 +300,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { { type: 'field_value_selection', field: 'namespace', - name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.namespaceColumnTitle', { defaultMessage: 'Namespace', }), multiSelect: 'or', @@ -313,7 +310,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { { type: 'field_value_selection', field: 'package', - name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + name: i18n.translate('xpack.fleet.dataStreamList.integrationColumnTitle', { defaultMessage: 'Integration', }), multiSelect: 'or', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx index b8fab92e40da8..87f83d39ad366 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx @@ -79,7 +79,7 @@ export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByT

    ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx index 0c1199f7c8867..ef3b94081b1d8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx @@ -75,7 +75,7 @@ export function PackageListGrid({ isLoading, controls, title, list }: ListProps)

    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts index f3520b4e7a9b3..735a5285baccd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts @@ -7,19 +7,19 @@ import { i18n } from '@kbn/i18n'; import { RegistryRelease } from '../../../types'; export const RELEASE_BADGE_LABEL: { [key in Exclude]: string } = { - beta: i18n.translate('xpack.ingestManager.epm.releaseBadge.betaLabel', { + beta: i18n.translate('xpack.fleet.epm.releaseBadge.betaLabel', { defaultMessage: 'Beta', }), - experimental: i18n.translate('xpack.ingestManager.epm.releaseBadge.experimentalLabel', { + experimental: i18n.translate('xpack.fleet.epm.releaseBadge.experimentalLabel', { defaultMessage: 'Experimental', }), }; export const RELEASE_BADGE_DESCRIPTION: { [key in Exclude]: string } = { - beta: i18n.translate('xpack.ingestManager.epm.releaseBadge.betaDescription', { + beta: i18n.translate('xpack.fleet.epm.releaseBadge.betaDescription', { defaultMessage: 'This integration is not recommended for use in production environments.', }), - experimental: i18n.translate('xpack.ingestManager.epm.releaseBadge.experimentalDescription', { + experimental: i18n.translate('xpack.fleet.epm.releaseBadge.experimentalDescription', { defaultMessage: 'This integration may have breaking changes or be removed in a future release.', }), }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx index 2342a79932c85..8ab738fd8ff58 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx @@ -73,14 +73,14 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar notifications.toasts.addWarning({ title: toMountPoint( ), text: toMountPoint( ), @@ -98,14 +98,14 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar notifications.toasts.addSuccess({ title: toMountPoint( ), text: toMountPoint( @@ -127,14 +127,14 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar notifications.toasts.addWarning({ title: toMountPoint( ), text: toMountPoint( ), @@ -146,14 +146,14 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar notifications.toasts.addSuccess({ title: toMountPoint( ), text: toMountPoint( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_install.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_install.tsx index ac30815a941ee..d34d947de33ab 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_install.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_install.tsx @@ -20,7 +20,7 @@ export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => { @@ -29,13 +29,13 @@ export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => { onConfirm={onConfirm} cancelButtonText={ } confirmButtonText={ @@ -46,7 +46,7 @@ export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => { iconType="iInCircle" title={ @@ -55,7 +55,7 @@ export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => {

    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_uninstall.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_uninstall.tsx index f4d5226ebe950..24ef18e1f979c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_uninstall.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/confirm_package_uninstall.tsx @@ -20,7 +20,7 @@ export const ConfirmPackageUninstall = (props: ConfirmPackageUninstallProps) => @@ -29,13 +29,13 @@ export const ConfirmPackageUninstall = (props: ConfirmPackageUninstallProps) => onConfirm={onConfirm} cancelButtonText={ } confirmButtonText={ @@ -47,7 +47,7 @@ export const ConfirmPackageUninstall = (props: ConfirmPackageUninstallProps) => color="danger" title={ @@ -55,7 +55,7 @@ export const ConfirmPackageUninstall = (props: ConfirmPackageUninstallProps) => >

    @@ -63,7 +63,7 @@ export const ConfirmPackageUninstall = (props: ConfirmPackageUninstallProps) =>

    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx index 84d3f97f3ee05..7e7958ce91afe 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx @@ -109,7 +109,7 @@ export function Detail() { href={getHref('integrations_all')} > @@ -161,7 +161,7 @@ export function Detail() { {[ { - label: i18n.translate('xpack.ingestManager.epm.versionLabel', { + label: i18n.translate('xpack.fleet.epm.versionLabel', { defaultMessage: 'Version', }), content: ( @@ -187,7 +187,7 @@ export function Detail() { })} > } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx index cdad67fd87548..306865075c63b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx @@ -66,7 +66,7 @@ export function InstallationButton(props: InstallationButtonProps) { {isInstalling ? ( ) : ( @@ -103,7 +103,7 @@ export function InstallationButton(props: InstallationButtonProps) { > {isRemoving ? ( ) : (

    - +

    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index 4ac6fe6872e17..814b05163d20c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -27,7 +27,7 @@ const UpdatesAvailableMsgContainer = styled.span` const NoteLabel = () => ( ); @@ -35,7 +35,7 @@ const UpdatesAvailableMsg = () => ( @@ -67,7 +67,7 @@ export const SettingsPanel = (

    @@ -78,7 +78,7 @@ export const SettingsPanel = (

    @@ -106,7 +106,7 @@ export const SettingsPanel = ( @@ -139,7 +139,7 @@ export const SettingsPanel = (

    @@ -193,7 +193,7 @@ export const SettingsPanel = ( {packageHasUsages && removable === true && (

    & { }; const PanelDisplayNames: Record = { - overview: i18n.translate('xpack.ingestManager.epm.packageDetailsNav.overviewLinkText', { + overview: i18n.translate('xpack.fleet.epm.packageDetailsNav.overviewLinkText', { defaultMessage: 'Overview', }), - usages: i18n.translate('xpack.ingestManager.epm.packageDetailsNav.packagePoliciesLinkText', { + usages: i18n.translate('xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText', { defaultMessage: 'Usages', }), - settings: i18n.translate('xpack.ingestManager.epm.packageDetailsNav.settingsLinkText', { + settings: i18n.translate('xpack.fleet.epm.packageDetailsNav.settingsLinkText', { defaultMessage: 'Settings', }), }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx index 363b1ede89e9e..e9704cd16b219 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx @@ -18,10 +18,7 @@ export const HeroCopy = memo(() => {

    - +

    @@ -29,7 +26,7 @@ export const HeroCopy = memo(() => {

    @@ -51,7 +48,7 @@ export const HeroImage = memo(() => { return ( ), @@ -91,7 +91,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ key="reassignPolicy" > , @@ -104,12 +104,12 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ > {isUnenrolling ? ( ) : ( )} @@ -122,7 +122,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ }} > , diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx index 2493fda3317d2..5ce757734e637 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx @@ -31,7 +31,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ {[ { - title: i18n.translate('xpack.ingestManager.agentDetails.hostNameLabel', { + title: i18n.translate('xpack.fleet.agentDetails.hostNameLabel', { defaultMessage: 'Host name', }), description: @@ -41,19 +41,19 @@ export const AgentDetailsContent: React.FunctionComponent<{ : '-', }, { - title: i18n.translate('xpack.ingestManager.agentDetails.hostIdLabel', { + title: i18n.translate('xpack.fleet.agentDetails.hostIdLabel', { defaultMessage: 'Agent ID', }), description: agent.id, }, { - title: i18n.translate('xpack.ingestManager.agentDetails.statusLabel', { + title: i18n.translate('xpack.fleet.agentDetails.statusLabel', { defaultMessage: 'Status', }), description: , }, { - title: i18n.translate('xpack.ingestManager.agentDetails.agentPolicyLabel', { + title: i18n.translate('xpack.fleet.agentDetails.agentPolicyLabel', { defaultMessage: 'Agent policy', }), description: agentPolicy ? ( @@ -68,7 +68,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ ), }, { - title: i18n.translate('xpack.ingestManager.agentDetails.versionLabel', { + title: i18n.translate('xpack.fleet.agentDetails.versionLabel', { defaultMessage: 'Agent version', }), description: @@ -85,7 +85,7 @@ export const AgentDetailsContent: React.FunctionComponent<{  
    @@ -97,7 +97,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ ), }, { - title: i18n.translate('xpack.ingestManager.agentDetails.releaseLabel', { + title: i18n.translate('xpack.fleet.agentDetails.releaseLabel', { defaultMessage: 'Agent release', }), description: @@ -110,7 +110,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ : '-', }, { - title: i18n.translate('xpack.ingestManager.agentDetails.platformLabel', { + title: i18n.translate('xpack.fleet.agentDetails.platformLabel', { defaultMessage: 'Platform', }), description: diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_events_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_events_table.tsx index e11863a2db85f..c1a1b3862728d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_events_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_events_table.tsx @@ -78,7 +78,7 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag @@ -92,7 +92,7 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag @@ -113,7 +113,7 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag const columns = [ { field: 'timestamp', - name: i18n.translate('xpack.ingestManager.agentEventsList.timestampColumnTitle', { + name: i18n.translate('xpack.fleet.agentEventsList.timestampColumnTitle', { defaultMessage: 'Timestamp', }), render: (timestamp: string) => ( @@ -132,7 +132,7 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag }, { field: 'type', - name: i18n.translate('xpack.ingestManager.agentEventsList.typeColumnTitle', { + name: i18n.translate('xpack.fleet.agentEventsList.typeColumnTitle', { defaultMessage: 'Type', }), width: '10%', @@ -141,7 +141,7 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag }, { field: 'subtype', - name: i18n.translate('xpack.ingestManager.agentEventsList.subtypeColumnTitle', { + name: i18n.translate('xpack.fleet.agentEventsList.subtypeColumnTitle', { defaultMessage: 'Subtype', }), width: '13%', @@ -150,7 +150,7 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag }, { field: 'message', - name: i18n.translate('xpack.ingestManager.agentEventsList.messageColumnTitle', { + name: i18n.translate('xpack.fleet.agentEventsList.messageColumnTitle', { defaultMessage: 'Message', }), render: (value: string) => ( @@ -168,10 +168,10 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag onClick={() => toggleDetails(agentEvent)} aria-label={ itemIdToExpandedRowMap[agentEvent.id] - ? i18n.translate('xpack.ingestManager.agentEventsList.collapseDetailsAriaLabel', { + ? i18n.translate('xpack.fleet.agentEventsList.collapseDetailsAriaLabel', { defaultMessage: 'Hide details', }) - : i18n.translate('xpack.ingestManager.agentEventsList.expandDetailsAriaLabel', { + : i18n.translate('xpack.fleet.agentEventsList.expandDetailsAriaLabel', { defaultMessage: 'Show details', }) } @@ -203,16 +203,15 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag value={search} onChange={setSearch} fieldPrefix={AGENT_EVENT_SAVED_OBJECT_TYPE} - placeholder={i18n.translate( - 'xpack.ingestManager.agentEventsList.searchPlaceholderText', - { defaultMessage: 'Search for activity logs' } - )} + placeholder={i18n.translate('xpack.fleet.agentEventsList.searchPlaceholderText', { + defaultMessage: 'Search for activity logs', + })} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx index 96c09677a677f..f808f4ade107b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx @@ -41,7 +41,7 @@ export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, fly

    @@ -51,7 +51,7 @@ export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, fly

    @@ -62,7 +62,7 @@ export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, fly

    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx index af7e8c674db4c..fd8de709c172a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx @@ -106,10 +106,7 @@ export const MetadataForm: React.FunctionComponent<{ agent: Agent }> = ({ agent const button = ( setOpen(true)} color={'text'}> - + ); return ( @@ -128,7 +125,7 @@ export const MetadataForm: React.FunctionComponent<{ agent: Agent }> = ({ agent @@ -137,7 +134,7 @@ export const MetadataForm: React.FunctionComponent<{ agent: Agent }> = ({ agent @@ -148,7 +145,7 @@ export const MetadataForm: React.FunctionComponent<{ agent: Agent }> = ({ agent diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/type_labels.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/type_labels.tsx index f597b9c72ab02..dbe18ab333736 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/type_labels.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/type_labels.tsx @@ -12,28 +12,25 @@ import { AgentEvent } from '../../../../types'; export const TYPE_LABEL: { [key in AgentEvent['type']]: JSX.Element } = { STATE: ( - + ), ERROR: ( - + ), ACTION_RESULT: ( ), ACTION: ( - + ), }; @@ -41,16 +38,13 @@ export const TYPE_LABEL: { [key in AgentEvent['type']]: JSX.Element } = { export const SUBTYPE_LABEL: { [key in AgentEvent['subtype']]: JSX.Element } = { RUNNING: ( - + ), STARTING: ( @@ -58,47 +52,38 @@ export const SUBTYPE_LABEL: { [key in AgentEvent['subtype']]: JSX.Element } = { IN_PROGRESS: ( ), CONFIG: ( - + ), FAILED: ( - + ), STOPPING: ( ), STOPPED: ( - + ), DEGRADED: ( @@ -106,7 +91,7 @@ export const SUBTYPE_LABEL: { [key in AgentEvent['subtype']]: JSX.Element } = { DATA_DUMP: ( @@ -114,7 +99,7 @@ export const SUBTYPE_LABEL: { [key in AgentEvent['subtype']]: JSX.Element } = { ACKNOWLEDGED: ( @@ -122,17 +107,14 @@ export const SUBTYPE_LABEL: { [key in AgentEvent['subtype']]: JSX.Element } = { UPDATING: ( ), UNKNOWN: ( - + ), }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx index ddd234aaa1da0..7d60ae23deac6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx @@ -89,7 +89,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { size="xs" > @@ -104,7 +104,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { agentData.item.local_metadata.host.hostname ) : ( { {[ { - label: i18n.translate('xpack.ingestManager.agentDetails.statusLabel', { + label: i18n.translate('xpack.fleet.agentDetails.statusLabel', { defaultMessage: 'Status', }), content: , }, { isDivider: true }, { - label: i18n.translate('xpack.ingestManager.agentDetails.policyLabel', { + label: i18n.translate('xpack.fleet.agentDetails.policyLabel', { defaultMessage: 'Policy', }), content: isAgentPolicyLoading ? ( @@ -150,7 +150,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { }, { isDivider: true }, { - label: i18n.translate('xpack.ingestManager.agentDetails.agentVersionLabel', { + label: i18n.translate('xpack.fleet.agentDetails.agentVersionLabel', { defaultMessage: 'Agent version', }), content: @@ -165,7 +165,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { { type="alert" color="warning" content={i18n.translate( - 'xpack.ingestManager.agentDetails.upgradeAvailableTooltip', + 'xpack.fleet.agentDetails.upgradeAvailableTooltip', { defaultMessage: 'Upgrade available', } @@ -225,7 +225,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { return [ { id: 'activity_log', - name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.activityLogTab', { + name: i18n.translate('xpack.fleet.agentDetails.subTabs.activityLogTab', { defaultMessage: 'Activity log', }), href: getHref('fleet_agent_details', { agentId, tabId: 'activity' }), @@ -233,7 +233,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { }, { id: 'details', - name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.detailsTab', { + name: i18n.translate('xpack.fleet.agentDetails.subTabs.detailsTab', { defaultMessage: 'Agent details', }), href: getHref('fleet_agent_details', { agentId, tabId: 'details' }), @@ -262,7 +262,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { } @@ -274,19 +274,16 @@ export const AgentDetailsPage: React.FunctionComponent = () => { } - error={i18n.translate( - 'xpack.ingestManager.agentDetails.agentNotFoundErrorDescription', - { - defaultMessage: 'Cannot find agent ID {agentId}', - values: { - agentId, - }, - } - )} + error={i18n.translate('xpack.fleet.agentDetails.agentNotFoundErrorDescription', { + defaultMessage: 'Cannot find agent ID {agentId}', + values: { + agentId, + }, + })} /> )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx index b628cc662f829..15b4a75188061 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx @@ -88,7 +88,7 @@ export const AgentBulkActions: React.FunctionComponent<{ { name: ( ), @@ -102,7 +102,7 @@ export const AgentBulkActions: React.FunctionComponent<{ { name: ( ), @@ -116,7 +116,7 @@ export const AgentBulkActions: React.FunctionComponent<{ { name: ( ), @@ -130,7 +130,7 @@ export const AgentBulkActions: React.FunctionComponent<{ { name: ( ), @@ -192,7 +192,7 @@ export const AgentBulkActions: React.FunctionComponent<{ {totalAgents > SO_SEARCH_LIMIT ? ( , @@ -201,7 +201,7 @@ export const AgentBulkActions: React.FunctionComponent<{ /> ) : ( @@ -226,7 +226,7 @@ export const AgentBulkActions: React.FunctionComponent<{ onClick={openMenu} > setSelectionMode('query')} > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index bc37338f04394..d46d2aa442745 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -53,26 +53,26 @@ const REFRESH_INTERVAL_MS = 5000; const statusFilters = [ { status: 'online', - label: i18n.translate('xpack.ingestManager.agentList.statusOnlineFilterText', { + label: i18n.translate('xpack.fleet.agentList.statusOnlineFilterText', { defaultMessage: 'Online', }), }, { status: 'offline', - label: i18n.translate('xpack.ingestManager.agentList.statusOfflineFilterText', { + label: i18n.translate('xpack.fleet.agentList.statusOfflineFilterText', { defaultMessage: 'Offline', }), }, , { status: 'error', - label: i18n.translate('xpack.ingestManager.agentList.statusErrorFilterText', { + label: i18n.translate('xpack.fleet.agentList.statusErrorFilterText', { defaultMessage: 'Error', }), }, { status: 'updating', - label: i18n.translate('xpack.ingestManager.agentList.statusUpdatingFilterText', { + label: i18n.translate('xpack.fleet.agentList.statusUpdatingFilterText', { defaultMessage: 'Updating', }), }, @@ -101,10 +101,7 @@ const RowActions = React.memo<{ href={getHref('fleet_agent_details', { agentId: agent.id })} key="viewAgent" > - + , , @@ -128,12 +125,12 @@ const RowActions = React.memo<{ > {isUnenrolling ? ( ) : ( )} @@ -146,7 +143,7 @@ const RowActions = React.memo<{ }} > , @@ -294,7 +291,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const columns = [ { field: 'local_metadata.host.hostname', - name: i18n.translate('xpack.ingestManager.agentList.hostColumnTitle', { + name: i18n.translate('xpack.fleet.agentList.hostColumnTitle', { defaultMessage: 'Host', }), render: (host: string, agent: Agent) => ( @@ -306,14 +303,14 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { { field: 'active', width: '120px', - name: i18n.translate('xpack.ingestManager.agentList.statusColumnTitle', { + name: i18n.translate('xpack.fleet.agentList.statusColumnTitle', { defaultMessage: 'Status', }), render: (active: boolean, agent: any) => , }, { field: 'policy_id', - name: i18n.translate('xpack.ingestManager.agentList.policyColumnTitle', { + name: i18n.translate('xpack.fleet.agentList.policyColumnTitle', { defaultMessage: 'Agent policy', }), render: (policyId: string, agent: Agent) => { @@ -333,7 +330,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { @@ -351,7 +348,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { {true && ( <> @@ -366,7 +363,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { { field: 'local_metadata.elastic.agent.version', width: '200px', - name: i18n.translate('xpack.ingestManager.agentList.versionTitle', { + name: i18n.translate('xpack.fleet.agentList.versionTitle', { defaultMessage: 'Version', }), render: (version: string, agent: Agent) => ( @@ -380,7 +377,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {   @@ -391,14 +388,14 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }, { field: 'last_checkin', - name: i18n.translate('xpack.ingestManager.agentList.lastCheckinTitle', { + name: i18n.translate('xpack.fleet.agentList.lastCheckinTitle', { defaultMessage: 'Last activity', }), render: (lastCheckin: string, agent: any) => lastCheckin ? : null, }, { - name: i18n.translate('xpack.ingestManager.agentList.actionsColumnTitle', { + name: i18n.translate('xpack.fleet.agentList.actionsColumnTitle', { defaultMessage: 'Actions', }), actions: [ @@ -425,7 +422,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { title={

    @@ -433,10 +430,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { actions={ hasWriteCapabilites ? ( setIsEnrollmentFlyoutOpen(true)}> - + ) : null } @@ -521,7 +515,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { disabled={isAgentPoliciesLoading} > @@ -561,7 +555,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { disabled={isAgentPoliciesLoading} > @@ -595,7 +589,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }} > @@ -604,7 +598,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { onClick={() => setShowInactive(!showInactive)} > @@ -645,18 +639,18 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { noItemsMessage={ isLoading && agentsRequest.isInitialRequest ? ( ) : isUsingFilter ? ( clearFilters()}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx index 874d42a8db095..758497607c057 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -155,7 +155,7 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { prepend={ @@ -172,10 +172,9 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { enrollmentAPIKeyId: undefined, }) } - aria-label={i18n.translate( - 'xpack.ingestManager.enrollmentStepAgentPolicy.policySelectAriaLabel', - { defaultMessage: 'Agent policy' } - )} + aria-label={i18n.translate('xpack.fleet.enrollmentStepAgentPolicy.policySelectAriaLabel', { + defaultMessage: 'Agent policy', + })} /> {selectedState.agentPolicyId && ( @@ -190,7 +189,7 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { onClick={() => setIsAuthenticationSettingsOpen(!isAuthenticationSettingsOpen)} > @@ -207,7 +206,7 @@ export const EnrollmentStepAgentPolicy: React.FC = (props) => { prepend={ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx index 05817f28f9292..cc7c2ea778951 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx @@ -41,7 +41,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({

    @@ -49,7 +49,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ @@ -57,13 +57,13 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ setMode('managed')}> setMode('standalone')}> @@ -82,7 +82,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ @@ -90,7 +90,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx index e620424f88f4e..656493e31e5f5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx @@ -45,7 +45,7 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { DownloadStep(), AgentPolicySelectionStep({ agentPolicies, setSelectedAPIKeyId }), { - title: i18n.translate('xpack.ingestManager.agentEnrollment.stepEnrollAndRunAgentTitle', { + title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', { defaultMessage: 'Enroll and start the Elastic Agent', }), children: apiKey.data && ( @@ -62,7 +62,7 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { <> @@ -74,13 +74,13 @@ export const ManagedInstructions = React.memo(({ agentPolicies }) => { ) : ( <> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx index 3e5cf236e63dc..a2daf2d10c271 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -75,14 +75,14 @@ export const StandaloneInstructions = React.memo(({ agentPolicies }) => { DownloadStep(), AgentPolicySelectionStep({ agentPolicies, setSelectedPolicyId }), { - title: i18n.translate('xpack.ingestManager.agentEnrollment.stepConfigureAgentTitle', { + title: i18n.translate('xpack.fleet.agentEnrollment.stepConfigureAgentTitle', { defaultMessage: 'Configure the agent', }), children: ( <> elastic-agent.yml, @@ -98,7 +98,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicies }) => { {(copy) => ( @@ -108,7 +108,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicies }) => { @@ -123,14 +123,14 @@ export const StandaloneInstructions = React.memo(({ agentPolicies }) => { ), }, { - title: i18n.translate('xpack.ingestManager.agentEnrollment.stepRunAgentTitle', { + title: i18n.translate('xpack.fleet.agentEnrollment.stepRunAgentTitle', { defaultMessage: 'Start the agent', }), children: ( <> @@ -140,7 +140,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicies }) => { {(copy) => ( @@ -151,20 +151,20 @@ export const StandaloneInstructions = React.memo(({ agentPolicies }) => { ), }, { - title: i18n.translate('xpack.ingestManager.agentEnrollment.stepCheckForDataTitle', { + title: i18n.translate('xpack.fleet.agentEnrollment.stepCheckForDataTitle', { defaultMessage: 'Check for data', }), children: ( <> @@ -181,7 +181,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicies }) => { <> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/steps.tsx index 0ae61ff421ffa..05ef733923d48 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/steps.tsx @@ -13,14 +13,14 @@ import { AgentPolicy } from '../../../../types'; export const DownloadStep = () => { return { - title: i18n.translate('xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle', { + title: i18n.translate('xpack.fleet.agentEnrollment.stepDownloadAgentTitle', { defaultMessage: 'Download the Elastic Agent to your host', }), children: ( <> @@ -32,7 +32,7 @@ export const DownloadStep = () => { iconType="popout" > @@ -51,7 +51,7 @@ export const AgentPolicySelectionStep = ({ setSelectedPolicyId?: (policyId: string) => void; }) => { return { - title: i18n.translate('xpack.ingestManager.agentEnrollment.stepChooseAgentPolicyTitle', { + title: i18n.translate('xpack.fleet.agentEnrollment.stepChooseAgentPolicyTitle', { defaultMessage: 'Choose an agent policy', }), children: ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx index a16d4e7347ad1..45017ac8532da 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx @@ -15,56 +15,38 @@ interface Props { const Status = { Online: ( - + ), Offline: ( - + ), Inactive: ( - + ), Warning: ( - + ), Error: ( - + ), Degraded: ( - + ), Enrolling: ( @@ -72,17 +54,14 @@ const Status = { Unenrolling: ( ), Upgrading: ( - + ), }; @@ -121,7 +100,7 @@ export const AgentHealth: React.FunctionComponent = ({ agent }) => { msLastCheckIn ? ( <> , @@ -133,7 +112,7 @@ export const AgentHealth: React.FunctionComponent = ({ agent }) => { ) : ( ) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_policy_package_badges.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_policy_package_badges.tsx index 0a3d6d54da834..08835cc872b82 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_policy_package_badges.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_policy_package_badges.tsx @@ -25,7 +25,7 @@ export const AgentPolicyPackageBadges: React.FunctionComponent = ({ agent <> = ({ } setIsSubmitting(false); const successMessage = i18n.translate( - 'xpack.ingestManager.agentReassignPolicy.successSingleNotificationTitle', + 'xpack.fleet.agentReassignPolicy.successSingleNotificationTitle', { defaultMessage: 'Agent policy reassigned', } @@ -97,7 +97,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({

    @@ -105,7 +105,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ = ({ @@ -146,7 +146,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ @@ -159,7 +159,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ isLoading={isSubmitting} > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_modal/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_modal/index.tsx index d756b1f4daf02..74f2303c70c0a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_modal/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_modal/index.tsx @@ -45,21 +45,19 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ setIsSubmitting(false); if (forceUnenroll) { const successMessage = isSingleAgent - ? i18n.translate( - 'xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle', - { defaultMessage: 'Agent unenrolled' } - ) - : i18n.translate( - 'xpack.ingestManager.unenrollAgents.successForceMultiNotificationTitle', - { defaultMessage: 'Agents unenrolled' } - ); + ? i18n.translate('xpack.fleet.unenrollAgents.successForceSingleNotificationTitle', { + defaultMessage: 'Agent unenrolled', + }) + : i18n.translate('xpack.fleet.unenrollAgents.successForceMultiNotificationTitle', { + defaultMessage: 'Agents unenrolled', + }); notifications.toasts.addSuccess(successMessage); } else { const successMessage = isSingleAgent - ? i18n.translate('xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', { + ? i18n.translate('xpack.fleet.unenrollAgents.successSingleNotificationTitle', { defaultMessage: 'Unenrolling agent', }) - : i18n.translate('xpack.ingestManager.unenrollAgents.successMultiNotificationTitle', { + : i18n.translate('xpack.fleet.unenrollAgents.successMultiNotificationTitle', { defaultMessage: 'Unenrolling agents', }); notifications.toasts.addSuccess(successMessage); @@ -68,7 +66,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ } catch (error) { setIsSubmitting(false); notifications.toasts.addError(error, { - title: i18n.translate('xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle', { + title: i18n.translate('xpack.fleet.unenrollAgents.fatalErrorNotificationTitle', { defaultMessage: 'Error unenrolling {count, plural, one {agent} other {agents}}', values: { count: agentCount }, }), @@ -82,12 +80,12 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ title={ isSingleAgent ? ( ) : ( @@ -97,7 +95,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ onConfirm={onSubmit} cancelButtonText={ } @@ -105,12 +103,12 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ confirmButtonText={ isSingleAgent ? ( ) : ( @@ -121,14 +119,14 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({

    {isSingleAgent ? ( ) : ( @@ -138,7 +136,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ legend={{ children: ( @@ -149,7 +147,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ id="ingestManagerForceUnenrollAgents" label={ = ({ } setIsSubmitting(false); const successMessage = isSingleAgent - ? i18n.translate('xpack.ingestManager.upgradeAgents.successSingleNotificationTitle', { + ? i18n.translate('xpack.fleet.upgradeAgents.successSingleNotificationTitle', { defaultMessage: 'Upgrading agent', }) - : i18n.translate('xpack.ingestManager.upgradeAgents.successMultiNotificationTitle', { + : i18n.translate('xpack.fleet.upgradeAgents.successMultiNotificationTitle', { defaultMessage: 'Upgrading agents', }); notifications.toasts.addSuccess(successMessage); @@ -53,7 +59,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ } catch (error) { setIsSubmitting(false); notifications.toasts.addError(error, { - title: i18n.translate('xpack.ingestManager.upgradeAgents.fatalErrorNotificationTitle', { + title: i18n.translate('xpack.fleet.upgradeAgents.fatalErrorNotificationTitle', { defaultMessage: 'Error upgrading {count, plural, one {agent} other {agents}}', values: { count: agentCount }, }), @@ -65,24 +71,44 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ - ) : ( - - ) + + + {isSingleAgent ? ( + + ) : ( + + )} + + + + } + tooltipContent={ + + } + /> + + } onCancel={onClose} onConfirm={onSubmit} cancelButtonText={ } @@ -90,12 +116,12 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ confirmButtonText={ isSingleAgent ? ( ) : ( @@ -105,8 +131,8 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({

    {isSingleAgent ? ( = ({ /> ) : ( )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx index 278beb5dfe35f..bf0163fe904e6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx @@ -53,7 +53,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { titleSize="xs" textAlign="right" title={} - description={i18n.translate('xpack.ingestManager.agentListStatus.totalLabel', { + description={i18n.translate('xpack.fleet.agentListStatus.totalLabel', { defaultMessage: 'Agents', })} /> @@ -79,7 +79,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { } - description={i18n.translate('xpack.ingestManager.agentListStatus.onlineLabel', { + description={i18n.translate('xpack.fleet.agentListStatus.onlineLabel', { defaultMessage: 'Online', })} /> @@ -89,7 +89,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { textAlign="right" titleSize="xs" title={} - description={i18n.translate('xpack.ingestManager.agentListStatus.offlineLabel', { + description={i18n.translate('xpack.fleet.agentListStatus.offlineLabel', { defaultMessage: 'Offline', })} /> @@ -99,7 +99,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { textAlign="right" titleSize="xs" title={} - description={i18n.translate('xpack.ingestManager.agentListStatus.errorLabel', { + description={i18n.translate('xpack.fleet.agentListStatus.errorLabel', { defaultMessage: 'Error', })} /> @@ -112,7 +112,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { setIsEnrollmentFlyoutOpen(true)}> @@ -126,7 +126,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => {

    - +

    @@ -134,7 +134,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => {

    @@ -159,19 +159,14 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { tabs={ ([ { - name: ( - - ), + name: , isSelected: routeMatch.path === PAGE_ROUTING_PATHS.fleet_agent_list, href: getHref('fleet_agent_list'), }, { name: ( ), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx index a115e03a369a2..45fd380a06f34 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx @@ -20,28 +20,22 @@ export const ConfirmEnrollmentTokenDelete = (props: Props) => { return ( = ({
    @@ -102,7 +102,7 @@ export const NewEnrollmentTokenFlyout: React.FunctionComponent = ({ @@ -118,7 +118,7 @@ export const NewEnrollmentTokenFlyout: React.FunctionComponent = ({ @@ -132,7 +132,7 @@ export const NewEnrollmentTokenFlyout: React.FunctionComponent = ({

    @@ -144,7 +144,7 @@ export const NewEnrollmentTokenFlyout: React.FunctionComponent = ({ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index f447469a02df2..7e5d07b2319d3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -73,10 +73,10 @@ const ApiKeyField: React.FunctionComponent<{ apiKeyId: string }> = ({ apiKeyId } = ({ apiKeyId } )} setState('CONFIRM_VISIBLE')} iconType="trash" color="danger" @@ -178,7 +175,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { const columns = [ { field: 'name', - name: i18n.translate('xpack.ingestManager.enrollmentTokensList.nameTitle', { + name: i18n.translate('xpack.fleet.enrollmentTokensList.nameTitle', { defaultMessage: 'Name', }), render: (value: string) => ( @@ -189,7 +186,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { }, { field: 'id', - name: i18n.translate('xpack.ingestManager.enrollmentTokensList.secretTitle', { + name: i18n.translate('xpack.fleet.enrollmentTokensList.secretTitle', { defaultMessage: 'Secret', }), width: '215px', @@ -199,7 +196,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { }, { field: 'policy_id', - name: i18n.translate('xpack.ingestManager.enrollmentTokensList.policyTitle', { + name: i18n.translate('xpack.fleet.enrollmentTokensList.policyTitle', { defaultMessage: 'Agent policy', }), render: (policyId: string) => { @@ -214,7 +211,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { }, { field: 'created_at', - name: i18n.translate('xpack.ingestManager.enrollmentTokensList.createdAtTitle', { + name: i18n.translate('xpack.fleet.enrollmentTokensList.createdAtTitle', { defaultMessage: 'Created on', }), width: '150px', @@ -226,7 +223,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { }, { field: 'active', - name: i18n.translate('xpack.ingestManager.enrollmentTokensList.activeTitle', { + name: i18n.translate('xpack.fleet.enrollmentTokensList.activeTitle', { defaultMessage: 'Active', }), width: '70px', @@ -237,7 +234,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { }, { field: 'actions', - name: i18n.translate('xpack.ingestManager.enrollmentTokensList.actionsTitle', { + name: i18n.translate('xpack.fleet.enrollmentTokensList.actionsTitle', { defaultMessage: 'Actions', }), width: '70px', @@ -267,7 +264,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { )} @@ -289,7 +286,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { setFlyoutOpen(true)}> @@ -302,12 +299,12 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { noItemsMessage={ enrollmentAPIKeysRequest.isLoading && enrollmentAPIKeysRequest.isInitialRequest ? ( ) : ( ) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/enforce_security.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/enforce_security.tsx index e131da159d6cb..7d547228f9c6c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/enforce_security.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/enforce_security.tsx @@ -11,14 +11,14 @@ import { NoDataLayout } from './components/no_data_layout'; export const EnforceSecurityPage = injectI18n(({ intl }) => (

    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/invalid_license.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/invalid_license.tsx index 883e41fea95b8..2632b6435cbd8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/invalid_license.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/invalid_license.tsx @@ -11,14 +11,14 @@ import { NoDataLayout } from './components/no_data_layout'; export const InvalidLicensePage = injectI18n(({ intl }) => (

    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/no_access.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/no_access.tsx index 5a3afd6216824..014ea6c04f2a2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/no_access.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/error_pages/no_access.tsx @@ -11,14 +11,14 @@ import { NoDataLayout } from './components/no_data_layout'; export const NoAccessPage = injectI18n(({ intl }) => (

    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx index bac551818ec87..884a2a12f71c5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx @@ -86,7 +86,7 @@ export const SetupPage: React.FunctionComponent<{

    @@ -94,7 +94,7 @@ export const SetupPage: React.FunctionComponent<{ @@ -102,7 +102,7 @@ export const SetupPage: React.FunctionComponent<{ @@ -119,26 +119,26 @@ export const SetupPage: React.FunctionComponent<{ @@ -161,7 +161,7 @@ export const SetupPage: React.FunctionComponent<{ xpack.security.authc.api_key.enabled, @@ -173,7 +173,7 @@ export const SetupPage: React.FunctionComponent<{ external > @@ -188,13 +188,13 @@ xpack.security.authc.api_key.enabled: true`} @@ -216,10 +216,7 @@ xpack.security.authc.api_key.enabled: true`} target="_blank" external > - + ), tlsFlag: xpack.fleet.agents.tlsCheckDisabled, @@ -234,7 +231,7 @@ xpack.security.authc.api_key.enabled: true`} )} > @@ -260,7 +257,7 @@ xpack.encryptedSavedObjects.encryptionKey: "something_at_least_32_characters"`} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx index e54eff1cbd4a5..c49f99cfe8a04 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_policy_section.tsx @@ -32,14 +32,14 @@ export const OverviewPolicySection: React.FC<{ agentPolicies: AgentPolicy[] }> = return ( @@ -50,7 +50,7 @@ export const OverviewPolicySection: React.FC<{ agentPolicies: AgentPolicy[] }> = <> @@ -59,7 +59,7 @@ export const OverviewPolicySection: React.FC<{ agentPolicies: AgentPolicy[] }> = diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx index 482105cdea300..5fbdf62d138d4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx @@ -25,15 +25,15 @@ export const OverviewAgentSection = () => { return ( @@ -44,7 +44,7 @@ export const OverviewAgentSection = () => { <> @@ -53,7 +53,7 @@ export const OverviewAgentSection = () => { @@ -62,7 +62,7 @@ export const OverviewAgentSection = () => { @@ -70,10 +70,7 @@ export const OverviewAgentSection = () => { - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx index bece6ec074b88..3b645f7aa9d66 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx @@ -46,14 +46,14 @@ export const OverviewDatastreamSection: React.FC = () => { return ( @@ -64,7 +64,7 @@ export const OverviewDatastreamSection: React.FC = () => { <> @@ -73,7 +73,7 @@ export const OverviewDatastreamSection: React.FC = () => { @@ -82,7 +82,7 @@ export const OverviewDatastreamSection: React.FC = () => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx index 1e138ae922afc..1ba1424d21b3a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx @@ -32,15 +32,15 @@ export const OverviewIntegrationSection: React.FC = () => { return ( @@ -51,7 +51,7 @@ export const OverviewIntegrationSection: React.FC = () => { <> @@ -60,7 +60,7 @@ export const OverviewIntegrationSection: React.FC = () => { @@ -69,7 +69,7 @@ export const OverviewIntegrationSection: React.FC = () => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index c2aa98358c3d3..c997caa90c653 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -45,20 +45,17 @@ export const IngestManagerOverview: React.FunctionComponent = () => {

    - +

    {

    @@ -83,7 +80,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => { setIsEnrollmentFlyoutOpen(true)}> diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index a6a514e85da0c..6847a39819e8e 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -82,7 +82,7 @@ export class IngestManagerPlugin core.application.register({ id: PLUGIN_ID, category: DEFAULT_APP_CATEGORIES.management, - title: i18n.translate('xpack.ingestManager.appTitle', { defaultMessage: 'Fleet' }), + title: i18n.translate('xpack.fleet.appTitle', { defaultMessage: 'Fleet' }), order: 9020, euiIconType: 'logoElastic', async mount(params: AppMountParameters) { @@ -109,10 +109,10 @@ export class IngestManagerPlugin deps.home.featureCatalogue.register({ id: 'ingestManager', - title: i18n.translate('xpack.ingestManager.featureCatalogueTitle', { + title: i18n.translate('xpack.fleet.featureCatalogueTitle', { defaultMessage: 'Add Elastic Agent', }), - description: i18n.translate('xpack.ingestManager.featureCatalogueDescription', { + description: i18n.translate('xpack.fleet.featureCatalogueDescription', { defaultMessage: 'Add and manage your fleet of Elastic Agents and integrations.', }), icon: 'indexManagementApp', diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts index fe06de765bbff..1ad710ba70e29 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts @@ -23,6 +23,12 @@ const fakeRequest = ({ url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; export async function agentPolicyUpdateEventHandler( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts index 994ecc64c82a7..b9ef36ecaae54 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts @@ -23,6 +23,12 @@ function getInternalUserSOClient() { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; return appContextService.getInternalUserSOClient(fakeRequest); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index aa48d8fe18e9f..c0e8540004930 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -58,6 +58,12 @@ function getInternalUserSOClient() { url: '/', }, }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, } as unknown) as KibanaRequest; return appContextService.getInternalUserSOClient(fakeRequest); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts index 89811783a7f79..1002eedc48740 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts @@ -17,6 +17,7 @@ import { CallESAsCurrentUser } from '../../../../types'; import { getInstallation } from '../../packages'; import { deleteTransforms, deleteTransformRefs } from './remove'; import { getAsset } from './common'; +import { appContextService } from '../../../app_context'; interface TransformInstallation { installationName: string; @@ -29,6 +30,7 @@ export const installTransform = async ( callCluster: CallESAsCurrentUser, savedObjectsClient: SavedObjectsClientContract ) => { + const logger = appContextService.getLogger(); const installation = await getInstallation({ savedObjectsClient, pkgName: installablePackage.name, @@ -38,6 +40,9 @@ export const installTransform = async ( previousInstalledTransformEsAssets = installation.installed_es.filter( ({ type, id }) => type === ElasticsearchAssetType.transform ); + logger.info( + `Found previous transform references:\n ${JSON.stringify(previousInstalledTransformEsAssets)}` + ); } // delete all previous transform diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts index 5b5583a121e5d..de7aeb816f76e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts @@ -24,6 +24,8 @@ export const deleteTransforms = async ( callCluster: CallESAsCurrentUser, transformIds: string[] ) => { + const logger = appContextService.getLogger(); + logger.info(`Deleting currently installed transform ids ${transformIds}`); await Promise.all( transformIds.map(async (transformId) => { // get the index the transform @@ -47,7 +49,7 @@ export const deleteTransforms = async ( path: `/_transform/${transformId}`, ignore: [404], }); - + logger.info(`Deleted: ${transformId}`); if (transformResponse?.transforms) { // expect this to be 1 for (const transform of transformResponse.transforms) { @@ -58,7 +60,7 @@ export const deleteTransforms = async ( }); } } else { - appContextService.getLogger().warn(`cannot find transform for ${transformId}`); + logger.warn(`cannot find transform for ${transformId}`); } }) ); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts index 2bf0ad12856f8..7ca2a32cf770c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createAppContextStartContractMock } from '../../../../mocks'; + jest.mock('../../packages/get', () => { return { getInstallation: jest.fn(), getInstallationObject: jest.fn() }; }); @@ -21,11 +23,13 @@ import { getInstallation, getInstallationObject } from '../../packages'; import { getAsset } from './common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { savedObjectsClientMock } from '../../../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { appContextService } from '../../../app_context'; describe('test transform install', () => { let legacyScopedClusterClient: jest.Mocked; let savedObjectsClient: jest.Mocked; beforeEach(() => { + appContextService.start(createAppContextStartContractMock()); legacyScopedClusterClient = { callAsInternalUser: jest.fn(), callAsCurrentUser: jest.fn(), diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index 2cf94e9c16079..cd0dcba7b97b2 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -86,7 +86,7 @@ export async function getPackageKeysByStatus( savedObjectsClient: SavedObjectsClientContract, status: InstallationStatus ) { - const allPackages = await getPackages({ savedObjectsClient }); + const allPackages = await getPackages({ savedObjectsClient, experimental: true }); return allPackages.reduce>((acc, pkg) => { if (pkg.status === status) { if (pkg.status === InstallationStatus.installed) { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx index 6dc7769ac02a9..9465117b6b589 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx @@ -9,10 +9,9 @@ import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SectionLoading, useKibana } from '../../../shared_imports'; +import { SectionLoading, useKibana, attemptToURIDecode } from '../../../shared_imports'; import { PipelinesCreate } from '../pipelines_create'; -import { attemptToURIDecode } from '../shared'; export interface ParamProps { sourceName: string; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx index 35ca1635ab9c3..7e2e85ab23fb3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -18,11 +18,10 @@ import { import { EuiCallOut } from '@elastic/eui'; import { Pipeline } from '../../../../common/types'; -import { useKibana, SectionLoading } from '../../../shared_imports'; +import { useKibana, SectionLoading, attemptToURIDecode } from '../../../shared_imports'; import { getListPath } from '../../services/navigation'; import { PipelineForm } from '../../components'; -import { attemptToURIDecode } from '../shared'; interface MatchParams { name: string; diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 13de8a74225ab..bea3055c13b2d 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -24,6 +24,7 @@ export { XJson, JsonEditor, OnJsonEditorUpdateHandler, + attemptToURIDecode, } from '../../../../src/plugins/es_ui_shared/public/'; export { diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 3943cbc54f0b3..ac5d145eedd5b 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -97,9 +97,12 @@ export async function mountApp( const redirectTo = (routeProps: RouteComponentProps<{ id?: string }>, savedObjectId?: string) => { if (!savedObjectId) { - routeProps.history.push('/'); + routeProps.history.push({ pathname: '/', search: routeProps.history.location.search }); } else { - routeProps.history.push(`/edit/${savedObjectId}`); + routeProps.history.push({ + pathname: `/edit/${savedObjectId}`, + search: routeProps.history.location.search, + }); } }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 5e5e9cda954ee..63ee02ac0404d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -32,16 +32,11 @@ import { ReactExpressionRendererType, } from '../../../../../../src/plugins/expressions/public'; import { prependDatasourceExpression } from './expression_helpers'; -import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; const MAX_SUGGESTIONS_DISPLAYED = 5; -// TODO: Remove this when upstream fix is merged https://github.com/elastic/eui/issues/2329 -// eslint-disable-next-line -const EuiPanelFixed = EuiPanel as React.ComponentType; - export interface SuggestionPanelProps { activeDatasourceId: string | null; datasourceMap: Record; @@ -82,6 +77,7 @@ const PreviewRenderer = ({ className="lnsSuggestionPanel__expressionRenderer" padding="s" expression={expression} + debounce={2000} renderError={() => { return (
    @@ -104,8 +100,6 @@ const PreviewRenderer = ({ ); }; -const DebouncedPreviewRenderer = debouncedComponent(PreviewRenderer, 2000); - const SuggestionPreview = ({ preview, ExpressionRenderer: ExpressionRendererComponent, @@ -126,7 +120,7 @@ const SuggestionPreview = ({ return (
    - {preview.expression ? ( - {preview.title} )} - +
    ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index fa63cd3c6f1e0..5cfc269dbb97b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -15,6 +15,7 @@ import { EuiPageContentHeader, EuiFlexGroup, EuiFlexItem, + EuiScreenReaderOnly, } from '@elastic/eui'; import { Datasource, FramePublicAPI, Visualization } from '../../../types'; import { NativeRenderer } from '../../../native_renderer'; @@ -104,18 +105,25 @@ export function WorkspacePanelWrapper({
    - {(!emptyExpression || title) && ( + {!emptyExpression || title ? ( - +

    {title || i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} - +

    + ) : ( + +

    + {title || + i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} +

    +
    )} {children} diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 3e05d4ddfbc20..9dc59eacd40d3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -172,6 +172,7 @@ describe('embeddable', () => { timeRange, query, filters, + searchSessionId: 'searchSessionId', }); expect(expressionRenderer).toHaveBeenCalledTimes(2); @@ -182,7 +183,13 @@ describe('embeddable', () => { const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; - const input = { savedObjectId: '123', timeRange, query, filters } as LensEmbeddableInput; + const input = { + savedObjectId: '123', + timeRange, + query, + filters, + searchSessionId: 'searchSessionId', + } as LensEmbeddableInput; const embeddable = new Embeddable( { @@ -214,6 +221,8 @@ describe('embeddable', () => { filters, }) ); + + expect(expressionRenderer.mock.calls[0][0].searchSessionId).toBe(input.searchSessionId); }); it('should merge external context with query and filters of the saved object', async () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index d245b7f2fcde4..10c243a272138 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -177,6 +177,7 @@ export class Embeddable ExpressionRenderer={this.expressionRenderer} expression={this.expression || null} searchContext={this.getMergedSearchContext()} + searchSessionId={this.input.searchSessionId} handleEvent={this.handleEvent} />, domNode diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index 4fb0630a305e7..484fe0436b0db 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -19,6 +19,7 @@ export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; expression: string | null; searchContext: ExecutionContextSearch; + searchSessionId?: string; handleEvent: (event: ExpressionRendererEvent) => void; } @@ -27,6 +28,7 @@ export function ExpressionWrapper({ expression, searchContext, handleEvent, + searchSessionId, }: ExpressionWrapperProps) { return ( @@ -48,9 +50,10 @@ export function ExpressionWrapper({
    (
    diff --git a/x-pack/plugins/lens/public/editor_frame_service/format_column.ts b/x-pack/plugins/lens/public/editor_frame_service/format_column.ts deleted file mode 100644 index 2da6e7195a5e1..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/format_column.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ExpressionFunctionDefinition, Datatable } from 'src/plugins/expressions/public'; - -interface FormatColumn { - format: string; - columnId: string; - decimals?: number; -} - -const supportedFormats: Record string }> = { - number: { - decimalsToPattern: (decimals = 2) => { - if (decimals === 0) { - return `0,0`; - } - return `0,0.${'0'.repeat(decimals)}`; - }, - }, - percent: { - decimalsToPattern: (decimals = 2) => { - if (decimals === 0) { - return `0,0%`; - } - return `0,0.${'0'.repeat(decimals)}%`; - }, - }, - bytes: { - decimalsToPattern: (decimals = 2) => { - if (decimals === 0) { - return `0,0b`; - } - return `0,0.${'0'.repeat(decimals)}b`; - }, - }, -}; - -export const formatColumn: ExpressionFunctionDefinition< - 'lens_format_column', - Datatable, - FormatColumn, - Datatable -> = { - name: 'lens_format_column', - type: 'datatable', - help: '', - args: { - format: { - types: ['string'], - help: '', - required: true, - }, - columnId: { - types: ['string'], - help: '', - required: true, - }, - decimals: { - types: ['number'], - help: '', - }, - }, - inputTypes: ['datatable'], - fn(input, { format, columnId, decimals }: FormatColumn) { - return { - ...input, - columns: input.columns.map((col) => { - if (col.id === columnId) { - if (supportedFormats[format]) { - return { - ...col, - meta: { - ...col.meta, - params: { - id: format, - params: { pattern: supportedFormats[format].decimalsToPattern(decimals) }, - }, - }, - }; - } else { - return { - ...col, - meta: { - ...col.meta, - params: { - id: format, - }, - }, - }; - } - } - return col; - }), - }; - }, -}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index e2a382133cb3c..d1df63780594e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -23,7 +23,6 @@ import { } from '../types'; import { Document } from '../persistence/saved_object_store'; import { mergeTables } from './merge_tables'; -import { formatColumn } from './format_column'; import { EmbeddableFactory, LensEmbeddableStartServices } from './embeddable/embeddable_factory'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { DashboardStart } from '../../../../../src/plugins/dashboard/public'; @@ -86,7 +85,6 @@ export class EditorFrameService { getAttributeService: () => Promise ): EditorFrameSetup { plugins.expressions.registerFunction(() => mergeTables); - plugins.expressions.registerFunction(() => formatColumn); const getStartServices = async (): Promise => { const [coreStart, deps] = await core.getStartServices(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 310548e5ab817..9500d4b44b79e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -435,7 +435,8 @@ export function DimensionEditor(props: DimensionEditorProps) { /> )} - {selectedColumn && selectedColumn.dataType === 'number' ? ( + {selectedColumn && + (selectedColumn.dataType === 'number' || selectedColumn.operationType === 'range') ? ( { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts new file mode 100644 index 0000000000000..3666528f43166 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ExpressionFunctionDefinition, + Datatable, + DatatableColumn, +} from 'src/plugins/expressions/public'; + +interface FormatColumn { + format: string; + columnId: string; + decimals?: number; + parentFormat?: string; +} + +export const supportedFormats: Record< + string, + { decimalsToPattern: (decimals?: number) => string } +> = { + number: { + decimalsToPattern: (decimals = 2) => { + if (decimals === 0) { + return `0,0`; + } + return `0,0.${'0'.repeat(decimals)}`; + }, + }, + percent: { + decimalsToPattern: (decimals = 2) => { + if (decimals === 0) { + return `0,0%`; + } + return `0,0.${'0'.repeat(decimals)}%`; + }, + }, + bytes: { + decimalsToPattern: (decimals = 2) => { + if (decimals === 0) { + return `0,0b`; + } + return `0,0.${'0'.repeat(decimals)}b`; + }, + }, +}; + +export const formatColumn: ExpressionFunctionDefinition< + 'lens_format_column', + Datatable, + FormatColumn, + Datatable +> = { + name: 'lens_format_column', + type: 'datatable', + help: '', + args: { + format: { + types: ['string'], + help: '', + required: true, + }, + columnId: { + types: ['string'], + help: '', + required: true, + }, + decimals: { + types: ['number'], + help: '', + }, + parentFormat: { + types: ['string'], + help: '', + }, + }, + inputTypes: ['datatable'], + fn(input, { format, columnId, decimals, parentFormat }: FormatColumn) { + return { + ...input, + columns: input.columns.map((col) => { + if (col.id === columnId) { + if (!parentFormat) { + if (supportedFormats[format]) { + return withParams(col, { + id: format, + params: { pattern: supportedFormats[format].decimalsToPattern(decimals) }, + }); + } else if (format) { + return withParams(col, { id: format }); + } else { + return col; + } + } + + const parsedParentFormat = JSON.parse(parentFormat); + const parentFormatId = parsedParentFormat.id; + const parentFormatParams = parsedParentFormat.params ?? {}; + + if (!parentFormatId) { + return col; + } + + if (format && supportedFormats[format]) { + return withParams(col, { + id: parentFormatId, + params: { + id: format, + params: { + pattern: supportedFormats[format].decimalsToPattern(decimals), + }, + ...parentFormatParams, + }, + }); + } + if (parentFormatParams) { + const innerParams = (col.meta.params?.params as Record) ?? {}; + return withParams(col, { + ...col.meta.params, + params: { + ...innerParams, + ...parentFormatParams, + }, + }); + } + } + return col; + }), + }; + }, +}; + +function withParams(col: DatatableColumn, params: Record) { + return { ...col, meta: { ...col.meta, params } }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 4fbed04112632..35987656f6670 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -33,8 +33,11 @@ export class IndexPatternDatasource { { expressions, editorFrame, charts }: IndexPatternDatasourceSetupPlugins ) { editorFrame.registerDatasource(async () => { - const { getIndexPatternDatasource, renameColumns } = await import('../async_services'); + const { getIndexPatternDatasource, renameColumns, formatColumn } = await import( + '../async_services' + ); expressions.registerFunction(renameColumns); + expressions.registerFunction(formatColumn); return core.getStartServices().then(([coreStart, { data }]) => getIndexPatternDatasource({ core: coreStart, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 900cd02622aaf..77dc6f97fb236 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -319,10 +319,10 @@ describe('IndexPattern Data Source', () => { "1", ], "metricsAtAllLevels": Array [ - true, + false, ], "partialRows": Array [ - true, + false, ], "timeFields": Array [ "timestamp", @@ -334,7 +334,7 @@ describe('IndexPattern Data Source', () => { Object { "arguments": Object { "idMap": Array [ - "{\\"col--1-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-2-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}", + "{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}", ], }, "function": "lens_rename_columns", @@ -392,6 +392,58 @@ describe('IndexPattern Data Source', () => { expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); }); + it('should rename the output from esaggs when using flat query', () => { + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['bucket1', 'bucket2', 'metric'], + columns: { + metric: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + bucket1: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: '1d', + }, + }, + bucket2: { + label: 'Terms', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'geo.src', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 10, + }, + }, + }, + }, + }, + }; + + const state = enrichBaseState(queryBaseState); + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.metricsAtAllLevels).toEqual([false]); + expect(JSON.parse(ast.chain[1].arguments.idMap[0] as string)).toEqual({ + 'col-0-bucket1': expect.any(Object), + 'col-1-bucket2': expect.any(Object), + 'col-2-metric': expect.any(Object), + }); + }); + it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => { const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 28aeac223e4a6..a6edfd71c93ca 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -107,6 +107,7 @@ export function uniqueLabels(layers: Record) { } export * from './rename_columns'; +export * from './format_column'; export function getIndexPatternDatasource({ core, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 06cfdf7e03481..4222c02388433 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -117,6 +117,7 @@ const indexPattern2 = ({ title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', hasRestrictions: true, + fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } }, fields: [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index fd8e071d524ee..70079cce6cc46 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -103,7 +103,14 @@ export async function loadIndexPatterns({ id: indexPattern.id!, // id exists for sure because we got index patterns by id title, timeFieldName, - fieldFormatMap, + fieldFormatMap: + fieldFormatMap && + Object.fromEntries( + Object.entries(fieldFormatMap).map(([id, format]) => [ + id, + 'toJSON' in format ? format.toJSON() : format, + ]) + ), fields: newFields, hasRestrictions: !!typeMeta?.aggs, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 21ed23321cf57..744a9f6743d09 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -82,6 +82,7 @@ export const createMockedRestrictedIndexPattern = () => ({ title: 'my-fake-restricted-pattern', timeFieldName: 'timestamp', hasRestrictions: true, + fieldFormatMap: { bytes: { id: 'bytes', params: { pattern: '0.0' } } }, fields: [ { name: 'timestamp', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.scss b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.scss index b1658043f3204..4af490e7479da 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.scss @@ -3,4 +3,8 @@ @include euiFontSizeS; min-height: $euiSizeXL; width: 100%; -} \ No newline at end of file +} + +.lnsRangesOperation__popoverNumberField { + width: 14ch; // Roughly 10 characters plus extra for the padding +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx index 96f4120e3df78..c6773e806aef8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx @@ -20,8 +20,8 @@ import { EuiPopover, EuiToolTip, htmlIdGenerator, + keys, } from '@elastic/eui'; -import { keys } from '@elastic/eui'; import { IFieldFormat } from '../../../../../../../../src/plugins/data/common'; import { RangeTypeLens, isValidRange, isValidNumber } from './ranges'; import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants'; @@ -39,8 +39,8 @@ type LocalRangeType = RangeTypeLens & { id: string }; const getBetterLabel = (range: RangeTypeLens, formatter: IFieldFormat) => range.label || formatter.convert({ - gte: isValidNumber(range.from) ? range.from : FROM_PLACEHOLDER, - lt: isValidNumber(range.to) ? range.to : TO_PLACEHOLDER, + gte: isValidNumber(range.from) ? range.from : -Infinity, + lt: isValidNumber(range.to) ? range.to : Infinity, }); export const RangePopover = ({ @@ -55,7 +55,6 @@ export const RangePopover = ({ Button: React.FunctionComponent<{ onClick: MouseEventHandler }>; isOpenByCreation: boolean; setIsOpenByCreation: (open: boolean) => void; - formatter: IFieldFormat; }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [tempRange, setTempRange] = useState(range); @@ -112,6 +111,7 @@ export const RangePopover = ({ { const newRange = { @@ -126,7 +126,6 @@ export const RangePopover = ({ {lteAppendLabel} } - fullWidth compressed placeholder={FROM_PLACEHOLDER} isInvalid={!isValidRange(tempRange)} @@ -137,6 +136,7 @@ export const RangePopover = ({ { const newRange = { @@ -151,7 +151,6 @@ export const RangePopover = ({ {ltPrependLabel} } - fullWidth compressed placeholder={TO_PLACEHOLDER} isInvalid={!isValidRange(tempRange)} @@ -180,6 +179,7 @@ export const RangePopover = ({ { defaultMessage: 'Custom label' } )} onSubmit={onSubmit} + compressed dataTestSubj="indexPattern-ranges-label" /> @@ -284,7 +284,6 @@ export const AdvancedRangeEditor = ({ } setLocalRanges(newRanges); }} - formatter={formatter} Button={({ onClick }: { onClick: MouseEventHandler }) => ( { - return { convert: ({ gte, lt }: { gte: string; lt: string }) => `${gte} - ${lt}` }; +dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ params }) => { + return { + convert: ({ gte, lt }: { gte: string; lt: string }) => { + if (params?.id === 'custom') { + return `Custom format: ${gte} - ${lt}`; + } + if (params?.id === 'bytes') { + return `Bytes format: ${gte} - ${lt}`; + } + return `${gte} - ${lt}`; + }, + }; }); type ReactMouseEvent = React.MouseEvent & @@ -74,7 +90,14 @@ describe('ranges', () => { function getDefaultState(): IndexPatternPrivateState { return { indexPatternRefs: [], - indexPatterns: {}, + indexPatterns: { + '1': { + id: '1', + title: 'my_index_pattern', + hasRestrictions: false, + fields: [{ name: sourceField, type: 'number', displayName: sourceField }], + }, + }, existingFields: {}, currentIndexPatternId: '1', isFirstExistenceFetch: false, @@ -396,7 +419,7 @@ describe('ranges', () => { /> ); - // This series of act clojures are made to make it work properly the update flush + // This series of act closures are made to make it work properly the update flush act(() => { instance.find(EuiButtonEmpty).prop('onClick')!({} as ReactMouseEvent); }); @@ -453,7 +476,7 @@ describe('ranges', () => { /> ); - // This series of act clojures are made to make it work properly the update flush + // This series of act closures are made to make it work properly the update flush act(() => { instance.find(EuiButtonEmpty).prop('onClick')!({} as ReactMouseEvent); }); @@ -510,7 +533,7 @@ describe('ranges', () => { /> ); - // This series of act clojures are made to make it work properly the update flush + // This series of act closures are made to make it work properly the update flush act(() => { instance.find(RangePopover).find(EuiLink).prop('onClick')!({} as ReactMouseEvent); }); @@ -667,6 +690,60 @@ describe('ranges', () => { ); }); }); + + it('should correctly handle the default formatter for the field', () => { + const setStateSpy = jest.fn(); + + // set a default formatter for the sourceField used + state.indexPatterns['1'].fieldFormatMap = { + MyField: { id: 'custom', params: {} }, + }; + + const instance = mount( + + ); + + expect(instance.find(RangePopover).find(EuiText).prop('children')).toMatch( + /^Custom format:/ + ); + }); + + it('should correctly pick the dimension formatter for the field', () => { + const setStateSpy = jest.fn(); + + // set a default formatter for the sourceField used + state.indexPatterns['1'].fieldFormatMap = { + MyField: { id: 'custom', params: {} }, + }; + + // now set a format on the range operation + (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.format = { + id: 'bytes', + params: { decimals: 0 }, + }; + + const instance = mount( + + ); + + expect(instance.find(RangePopover).find(EuiText).prop('children')).toMatch( + /^Bytes format:/ + ); + }); }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index a256f5e4ecfa1..1050eef45a71c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -13,7 +13,9 @@ import { RangeEditor } from './range_editor'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { updateColumnParam, changeColumn } from '../../../state_helpers'; +import { supportedFormats } from '../../../format_column'; import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants'; +import { IndexPattern, IndexPatternField } from '../../../types'; type RangeType = Omit; // Try to cover all possible serialized states for ranges @@ -32,6 +34,11 @@ export interface RangeIndexPatternColumn extends FieldBasedIndexPatternColumn { type: MODES_TYPES; maxBars: typeof AUTO_BARS | number; ranges: RangeTypeLens[]; + format?: { id: string; params?: { decimals: number } }; + parentFormat?: { + id: string; + params?: { id?: string; template?: string; replaceInfinity?: boolean }; + }; }; } @@ -55,6 +62,15 @@ export const isValidRange = (range: RangeTypeLens): boolean => { return true; }; +function getFieldDefaultFormat(indexPattern: IndexPattern, field: IndexPatternField | undefined) { + if (field) { + if (indexPattern.fieldFormatMap && indexPattern.fieldFormatMap[field.name]) { + return indexPattern.fieldFormatMap[field.name]; + } + } + return undefined; +} + function getEsAggsParams({ sourceField, params }: RangeIndexPatternColumn) { if (params.type === MODES.Range) { return { @@ -105,7 +121,7 @@ export const rangeOperation: OperationDefinition { - const rangeFormatter = data.fieldFormats.deserialize({ id: 'range' }); + const indexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; + const currentField = indexPattern.fields.find( + (field) => field.name === currentColumn.sourceField + ); + const numberFormat = currentColumn.params.format; + const numberFormatterPattern = + numberFormat && + supportedFormats[numberFormat.id] && + supportedFormats[numberFormat.id].decimalsToPattern(numberFormat.params?.decimals || 0); + + const rangeFormatter = data.fieldFormats.deserialize({ + ...currentColumn.params.parentFormat, + params: { + ...currentColumn.params.parentFormat?.params, + ...(numberFormat + ? { id: numberFormat.id, params: { pattern: numberFormatterPattern } } + : getFieldDefaultFormat(indexPattern, currentField)), + }, + }); + const MAX_HISTOGRAM_BARS = uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); const granularityStep = (MAX_HISTOGRAM_BARS - MIN_HISTOGRAM_BARS) / SLICES; const maxBarsDefaultValue = (MAX_HISTOGRAM_BARS - MIN_HISTOGRAM_BARS) / 2; @@ -171,6 +208,10 @@ export const rangeOperation: OperationDefinition { const scale = newMode === MODES.Range ? 'ordinal' : 'interval'; const dataType = newMode === MODES.Range ? 'string' : 'number'; + const parentFormat = + newMode === MODES.Range + ? { id: 'range', params: { template: 'arrow_right', replaceInfinity: true } } + : undefined; setState( changeColumn({ state, @@ -184,6 +225,8 @@ export const rangeOperation: OperationDefinition void; @@ -23,6 +24,7 @@ export const LabelInput = ({ inputRef?: React.MutableRefObject; onSubmit?: () => void; dataTestSubj?: string; + compressed?: boolean; }) => { const [inputValue, setInputValue] = useState(value); @@ -57,6 +59,7 @@ export const LabelInput = ({ prepend={i18n.translate('xpack.lens.labelInput.label', { defaultMessage: 'Label', })} + compressed={compressed} /> ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 1b87c48dc7193..ea7aa62054e5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -29,66 +29,66 @@ function getExpressionForLayer( } const columnEntries = columnOrder.map((colId) => [colId, columns[colId]] as const); - const bucketsCount = columnEntries.filter(([, entry]) => entry.isBucketed).length; - const metricsCount = columnEntries.length - bucketsCount; if (columnEntries.length) { const aggs = columnEntries.map(([colId, col]) => { return getEsAggsConfig(col, colId); }); - /** - * Because we are turning on metrics at all levels, the sequence generation - * logic here is more complicated. Examples follow: - * - * Example 1: [Count] - * Output: [`col-0-count`] - * - * Example 2: [Terms, Terms, Count] - * Output: [`col-0-terms0`, `col-2-terms1`, `col-3-count`] - * - * Example 3: [Terms, Terms, Count, Max] - * Output: [`col-0-terms0`, `col-3-terms1`, `col-4-count`, `col-5-max`] - */ const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => { - const newIndex = column.isBucketed - ? index * (metricsCount + 1) // Buckets are spaced apart by N + 1 - : (index ? index + 1 : 0) - bucketsCount + (bucketsCount - 1) * (metricsCount + 1); return { ...currentIdMap, - [`col-${columnEntries.length === 1 ? 0 : newIndex}-${colId}`]: { + [`col-${columnEntries.length === 1 ? 0 : index}-${colId}`]: { ...column, id: colId, }, }; }, {} as Record); - type FormattedColumn = Required>; + type FormattedColumn = Required< + Extract< + IndexPatternColumn, + | { + params?: { + format: unknown; + }; + } + // when formatters are nested there's a slightly different format + | { + params: { + format?: unknown; + parentFormat?: unknown; + }; + } + > + >; const columnsWithFormatters = columnEntries.filter( - ([, col]) => col.params && 'format' in col.params && col.params.format + ([, col]) => + col.params && + (('format' in col.params && col.params.format) || + ('parentFormat' in col.params && col.params.parentFormat)) ) as Array<[string, FormattedColumn]>; - const formatterOverrides: ExpressionFunctionAST[] = columnsWithFormatters.map(([id, col]) => { - const format = (col as FormattedColumn).params!.format; - const base: ExpressionFunctionAST = { - type: 'function', - function: 'lens_format_column', - arguments: { - format: [format.id], - columnId: [id], - }, - }; - if (typeof format.params?.decimals === 'number') { - return { - ...base, + const formatterOverrides: ExpressionFunctionAST[] = columnsWithFormatters.map( + ([id, col]: [string, FormattedColumn]) => { + // TODO: improve the type handling here + const parentFormat = 'parentFormat' in col.params ? col.params!.parentFormat! : undefined; + const format = (col as FormattedColumn).params!.format; + + const base: ExpressionFunctionAST = { + type: 'function', + function: 'lens_format_column', arguments: { - ...base.arguments, - decimals: [format.params.decimals], + format: format ? [format.id] : [''], + columnId: [id], + decimals: typeof format?.params?.decimals === 'number' ? [format.params.decimals] : [], + parentFormat: parentFormat ? [JSON.stringify(parentFormat)] : [], }, }; + + return base; } - return base; - }); + ); const allDateHistogramFields = Object.values(columns) .map((column) => @@ -104,8 +104,8 @@ function getExpressionForLayer( function: 'esaggs', arguments: { index: [indexPattern.id], - metricsAtAllLevels: [true], - partialRows: [true], + metricsAtAllLevels: [false], + partialRows: [false], includeFormatHints: [true], timeFields: allDateHistogramFields, aggConfigs: [JSON.stringify(aggs)], diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index cb2458a76967c..d4c85ce9b8843 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -27,8 +27,8 @@ import { import { FormatFactory, LensFilterEvent } from '../types'; import { VisualizationContainer } from '../visualization_container'; import { CHART_NAMES, DEFAULT_PERCENT_DECIMALS } from './constants'; -import { ColumnGroups, PieExpressionProps } from './types'; -import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; +import { PieExpressionProps } from './types'; +import { getSliceValue, getFilterContext } from './render_helpers'; import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; import { desanitizeFilterContext } from '../utils'; @@ -72,21 +72,6 @@ export function PieComponent( }); } - // The datatable for pie charts should include subtotals, like this: - // [bucket, subtotal, bucket, count] - // But the user only configured [bucket, bucket, count] - const columnGroups: ColumnGroups = []; - firstTable.columns.forEach((col) => { - if (groups.includes(col.id)) { - columnGroups.push({ - col, - metrics: [], - }); - } else if (columnGroups.length > 0) { - columnGroups[columnGroups.length - 1].metrics.push(col); - } - }); - const fillLabel: Partial = { textInvertible: false, valueFont: { @@ -100,7 +85,9 @@ export function PieComponent( fillLabel.valueFormatter = () => ''; } - const layers: PartitionLayer[] = columnGroups.map(({ col }, layerIndex) => { + const bucketColumns = firstTable.columns.filter((col) => groups.includes(col.id)); + + const layers: PartitionLayer[] = bucketColumns.map((col, layerIndex) => { return { groupByRollup: (d: Datum) => d[col.id] ?? EMPTY_SLICE, showAccessor: (d: Datum) => d !== EMPTY_SLICE, @@ -116,7 +103,7 @@ export function PieComponent( fillLabel: isDarkMode && shape === 'treemap' && - layerIndex < columnGroups.length - 1 && + layerIndex < bucketColumns.length - 1 && categoryDisplay !== 'hide' ? { ...fillLabel, textColor: euiDarkVars.euiTextColor } : fillLabel, @@ -136,10 +123,10 @@ export function PieComponent( if (shape === 'treemap') { // Only highlight the innermost color of the treemap, as it accurately represents area - return layerIndex < columnGroups.length - 1 ? 'rgba(0,0,0,0)' : outputColor; + return layerIndex < bucketColumns.length - 1 ? 'rgba(0,0,0,0)' : outputColor; } - const lighten = (d.depth - 1) / (columnGroups.length * 2); + const lighten = (d.depth - 1) / (bucketColumns.length * 2); return color(outputColor, 'hsl').lighten(lighten).hex(); }, }, @@ -198,8 +185,6 @@ export function PieComponent( setState({ isReady: true }); }, []); - const reverseGroups = [...columnGroups].reverse(); - const hasNegative = firstTable.rows.some((row) => { const value = row[metricColumn.id]; return typeof value === 'number' && value < 0; @@ -243,16 +228,12 @@ export function PieComponent( showLegend={ !hideLabels && (legendDisplay === 'show' || - (legendDisplay === 'default' && columnGroups.length > 1 && shape !== 'treemap')) + (legendDisplay === 'default' && bucketColumns.length > 1 && shape !== 'treemap')) } legendPosition={legendPosition || Position.Right} legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */} onElementClick={(args) => { - const context = getFilterContext( - args[0][0] as LayerValue[], - columnGroups.map(({ col }) => col.id), - firstTable - ); + const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable); onClickValue(desanitizeFilterContext(context)); }} @@ -262,7 +243,7 @@ export function PieComponent( getSliceValueWithFallback(d, reverseGroups, metricColumn)} + valueAccessor={(d: Datum) => getSliceValue(d, metricColumn)} percentFormatter={(d: number) => percentFormatter.convert(d / 100)} valueGetter={hideLabels || numberDisplay === 'value' ? undefined : 'percent'} valueFormatter={(d: number) => (hideLabels ? '' : formatters[metricColumn.id].convert(d))} diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts index d9ccda2a99ab2..22c63cd67281b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts @@ -5,86 +5,47 @@ */ import { Datatable } from 'src/plugins/expressions/public'; -import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; -import { ColumnGroups } from './types'; +import { getSliceValue, getFilterContext } from './render_helpers'; describe('render helpers', () => { - describe('#getSliceValueWithFallback', () => { - describe('without fallback', () => { - const columnGroups: ColumnGroups = [ - { col: { id: 'a', name: 'A', meta: { type: 'string' } }, metrics: [] }, - { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] }, - ]; - - it('returns the metric when positive number', () => { - expect( - getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 5 }, columnGroups, { + describe('#getSliceValue', () => { + it('returns the metric when positive number', () => { + expect( + getSliceValue( + { a: 'Cat', b: 'Home', c: 5 }, + { id: 'c', name: 'C', meta: { type: 'number' }, - }) - ).toEqual(5); - }); + } + ) + ).toEqual(5); + }); - it('returns the metric when negative number', () => { - expect( - getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: -100 }, columnGroups, { + it('returns the metric when negative number', () => { + expect( + getSliceValue( + { a: 'Cat', b: 'Home', c: -100 }, + { id: 'c', name: 'C', meta: { type: 'number' }, - }) - ).toEqual(-100); - }); + } + ) + ).toEqual(-100); + }); - it('returns epsilon when metric is 0 without fallback', () => { - expect( - getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 0 }, columnGroups, { + it('returns epsilon when metric is 0 without fallback', () => { + expect( + getSliceValue( + { a: 'Cat', b: 'Home', c: 0 }, + { id: 'c', name: 'C', meta: { type: 'number' }, - }) - ).toEqual(Number.EPSILON); - }); - }); - - describe('fallback behavior', () => { - const columnGroups: ColumnGroups = [ - { - col: { id: 'a', name: 'A', meta: { type: 'string' } }, - metrics: [{ id: 'a_subtotal', name: '', meta: { type: 'number' } }], - }, - { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] }, - ]; - - it('falls back to metric from previous column if available', () => { - expect( - getSliceValueWithFallback( - { a: 'Cat', a_subtotal: 5, b: 'Home', c: undefined }, - columnGroups, - { id: 'c', name: 'C', meta: { type: 'number' } } - ) - ).toEqual(5); - }); - - it('uses epsilon if fallback is 0', () => { - expect( - getSliceValueWithFallback( - { a: 'Cat', a_subtotal: 0, b: 'Home', c: undefined }, - columnGroups, - { id: 'c', name: 'C', meta: { type: 'number' } } - ) - ).toEqual(Number.EPSILON); - }); - - it('uses epsilon if fallback is missing', () => { - expect( - getSliceValueWithFallback( - { a: 'Cat', a_subtotal: undefined, b: 'Home', c: undefined }, - columnGroups, - { id: 'c', name: 'C', meta: { type: 'number' } } - ) - ).toEqual(Number.EPSILON); - }); + } + ) + ).toEqual(Number.EPSILON); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts index 26b4f9ccda853..978afcca6a550 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts @@ -6,22 +6,13 @@ import { Datum, LayerValue } from '@elastic/charts'; import { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; -import { ColumnGroups } from './types'; import { LensFilterEvent } from '../types'; -export function getSliceValueWithFallback( - d: Datum, - reverseGroups: ColumnGroups, - metricColumn: DatatableColumn -) { +export function getSliceValue(d: Datum, metricColumn: DatatableColumn) { if (typeof d[metricColumn.id] === 'number' && d[metricColumn.id] !== 0) { return d[metricColumn.id]; } - // Sometimes there is missing data for outer groups - // When there is missing data, we fall back to the next groups - // This creates a sunburst effect - const hasMetric = reverseGroups.find((group) => group.metrics.length && d[group.metrics[0].id]); - return hasMetric ? d[hasMetric.metrics[0].id] || Number.EPSILON : Number.EPSILON; + return Number.EPSILON; } export function getFilterContext( diff --git a/x-pack/plugins/lens/public/pie_visualization/types.ts b/x-pack/plugins/lens/public/pie_visualization/types.ts index 0596e54870a94..54bececa13c2a 100644 --- a/x-pack/plugins/lens/public/pie_visualization/types.ts +++ b/x-pack/plugins/lens/public/pie_visualization/types.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DatatableColumn } from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; export interface SharedLayerState { @@ -38,8 +37,3 @@ export interface PieExpressionProps { data: LensMultiTable; args: PieExpressionArgs; } - -export type ColumnGroups = Array<{ - col: DatatableColumn; - metrics: DatatableColumn[]; -}>; diff --git a/x-pack/plugins/lens/server/plugin.tsx b/x-pack/plugins/lens/server/plugin.tsx index b801d30f5ba9b..a8f9bef92349c 100644 --- a/x-pack/plugins/lens/server/plugin.tsx +++ b/x-pack/plugins/lens/server/plugin.tsx @@ -29,13 +29,13 @@ export class LensServerPlugin implements Plugin<{}, {}, {}, {}> { private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; private readonly telemetryLogger: Logger; - constructor(initializerContext: PluginInitializerContext) { + constructor(private initializerContext: PluginInitializerContext) { this.kibanaIndexConfig = initializerContext.config.legacy.globalConfig$; this.telemetryLogger = initializerContext.logger.get('usage'); } setup(core: CoreSetup, plugins: PluginSetupContract) { setupSavedObjects(core); - setupRoutes(core); + setupRoutes(core, this.initializerContext.logger.get()); if (plugins.usageCollection && plugins.taskManager) { registerLensUsageCollector( plugins.usageCollection, diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index c925517b572da..d3b2314a199cb 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -7,10 +7,14 @@ import Boom from 'boom'; import { schema } from '@kbn/config-schema'; import { ILegacyScopedClusterClient, SavedObject, RequestHandlerContext } from 'src/core/server'; -import { CoreSetup } from 'src/core/server'; +import { CoreSetup, Logger } from 'src/core/server'; import { BASE_API_URL } from '../../common'; import { IndexPatternAttributes, UI_SETTINGS } from '../../../../../src/plugins/data/server'; +export function isBoomError(error: { isBoom?: boolean }): error is Boom { + return error.isBoom === true; +} + /** * The number of docs to sample to determine field empty status. */ @@ -24,7 +28,7 @@ export interface Field { script?: string; } -export async function existingFieldsRoute(setup: CoreSetup) { +export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { const router = setup.http.createRouter(); router.post( @@ -52,14 +56,17 @@ export async function existingFieldsRoute(setup: CoreSetup) { }), }); } catch (e) { + logger.info( + `Field existence check failed: ${isBoomError(e) ? e.output.payload.message : e.message}` + ); if (e.status === 404) { - return res.notFound(); + return res.notFound({ body: e.message }); } - if (e.isBoom) { + if (isBoomError(e)) { if (e.output.statusCode === 404) { - return res.notFound(); + return res.notFound({ body: e.output.payload.message }); } - return res.internalError(e.output.message); + return res.internalError({ body: e.output.payload.message }); } else { return res.internalError({ body: Boom.internal(e.message || e.name), diff --git a/x-pack/plugins/lens/server/routes/index.ts b/x-pack/plugins/lens/server/routes/index.ts index 8bc04a56e16d5..01018d8cd7fe5 100644 --- a/x-pack/plugins/lens/server/routes/index.ts +++ b/x-pack/plugins/lens/server/routes/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/server'; +import { CoreSetup, Logger } from 'src/core/server'; import { existingFieldsRoute } from './existing_fields'; import { initFieldsRoute } from './field_stats'; import { initLensUsageRoute } from './telemetry'; -export function setupRoutes(setup: CoreSetup) { - existingFieldsRoute(setup); +export function setupRoutes(setup: CoreSetup, logger: Logger) { + existingFieldsRoute(setup, logger); initFieldsRoute(setup); initLensUsageRoute(setup); } diff --git a/x-pack/plugins/license_management/README.md b/x-pack/plugins/license_management/README.md new file mode 100644 index 0000000000000..b103c8fcd6721 --- /dev/null +++ b/x-pack/plugins/license_management/README.md @@ -0,0 +1,5 @@ +# License Management + +## About + +This plugin enables users to activate a trial license, downgrade to Basic, and upload a new license. \ No newline at end of file diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts index b2390ea35c140..eb07a07d3b52f 100644 --- a/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { FeatureUsageService, diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts index f247c6ffcb526..b882219b2e29d 100644 --- a/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { FeatureUsageService, diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index 57f83b9533bda..c8c9f6ba40041 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -441,7 +441,7 @@ export function clearMissingStyleProperties(layerId: string) { const { hasChanges, nextStyleDescriptor, - } = (style as IVectorStyle).getDescriptorWithMissingStylePropsRemoved( + } = await (style as IVectorStyle).getDescriptorWithMissingStylePropsRemoved( nextFields, getMapColors(getState()) ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js index 53a3fc95adbeb..d577912efb830 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js @@ -20,20 +20,16 @@ import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch } from '@elastic/eui'; import { - CATEGORICAL_DATA_TYPES, - ORDINAL_DATA_TYPES, LABEL_BORDER_SIZES, VECTOR_STYLES, STYLE_TYPE, VECTOR_SHAPE_TYPE, } from '../../../../../common/constants'; +import { createStyleFieldsHelper } from '../style_fields_helper'; export class VectorStyleEditor extends Component { state = { - dateFields: [], - numberFields: [], - fields: [], - ordinalAndCategoricalFields: [], + styleFields: [], defaultDynamicProperties: getDefaultDynamicProperties(), defaultStaticProperties: getDefaultStaticProperties(), supportedFeatures: undefined, @@ -56,33 +52,17 @@ export class VectorStyleEditor extends Component { } async _loadFields() { - const getFieldMeta = async (field) => { - return { - label: await field.getLabel(), - name: field.getName(), - origin: field.getOrigin(), - type: await field.getDataType(), - supportsAutoDomain: field.supportsAutoDomain(), - }; - }; - - //These are all fields (only used for text labeling) - const fields = await this.props.layer.getStyleEditorFields(); - const fieldPromises = fields.map(getFieldMeta); - const fieldsArrayAll = await Promise.all(fieldPromises); - if (!this._isMounted || _.isEqual(fieldsArrayAll, this.state.fields)) { + const styleFieldsHelper = await createStyleFieldsHelper( + await this.props.layer.getStyleEditorFields() + ); + const styleFields = styleFieldsHelper.getStyleFields(); + if (!this._isMounted || _.isEqual(styleFields, this.state.styleFields)) { return; } this.setState({ - fields: fieldsArrayAll, - ordinalAndCategoricalFields: fieldsArrayAll.filter((field) => { - return ( - CATEGORICAL_DATA_TYPES.includes(field.type) || ORDINAL_DATA_TYPES.includes(field.type) - ); - }), - dateFields: fieldsArrayAll.filter((field) => field.type === 'date'), - numberFields: fieldsArrayAll.filter((field) => field.type === 'number'), + styleFields, + styleFieldsHelper, }); } @@ -109,12 +89,6 @@ export class VectorStyleEditor extends Component { } } - _getOrdinalFields() { - return [...this.state.dateFields, ...this.state.numberFields].filter((field) => { - return field.supportsAutoDomain; - }); - } - _handleSelectedFeatureChange = (selectedFeature) => { this.setState({ selectedFeature }); }; @@ -165,7 +139,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.FILL_COLOR]} - fields={this.state.ordinalAndCategoricalFields} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.FILL_COLOR)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.FILL_COLOR].options } @@ -186,7 +160,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_COLOR]} - fields={this.state.ordinalAndCategoricalFields} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LINE_COLOR)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LINE_COLOR].options } @@ -205,7 +179,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_WIDTH]} - fields={this._getOrdinalFields()} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LINE_WIDTH)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LINE_WIDTH].options } @@ -225,7 +199,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_TEXT]} - fields={this.state.fields} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LABEL_TEXT)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_TEXT].options } @@ -242,7 +216,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_COLOR]} - fields={this.state.ordinalAndCategoricalFields} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LABEL_COLOR)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_COLOR].options } @@ -258,7 +232,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_SIZE]} - fields={this._getOrdinalFields()} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LABEL_SIZE)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_SIZE].options } @@ -275,7 +249,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_COLOR]} - fields={this.state.ordinalAndCategoricalFields} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LABEL_BORDER_COLOR)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_BORDER_COLOR].options } @@ -309,7 +283,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.ICON_ORIENTATION]} - fields={this.state.numberFields} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.ICON_ORIENTATION)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.ICON_ORIENTATION].options } @@ -328,7 +302,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.ICON]} - fields={this.state.ordinalAndCategoricalFields} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.ICON)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.ICON].options } @@ -368,7 +342,7 @@ export class VectorStyleEditor extends Component { onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} styleProperty={this.props.styleProperties[VECTOR_STYLES.ICON_SIZE]} - fields={this._getOrdinalFields()} + fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.ICON_SIZE)} defaultStaticStyleOptions={ this.state.defaultStaticProperties[VECTOR_STYLES.ICON_SIZE].options } @@ -409,9 +383,9 @@ export class VectorStyleEditor extends Component { } _renderProperties() { - const { supportedFeatures, selectedFeature } = this.state; + const { supportedFeatures, selectedFeature, styleFieldsHelper } = this.state; - if (!supportedFeatures) { + if (!supportedFeatures || !styleFieldsHelper) { return null; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts index a2dfdc94d8058..a3f63ba7717ea 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__tests__/test_util.ts @@ -16,10 +16,38 @@ import { import { AbstractField, IField } from '../../../../fields/field'; import { IStyle } from '../../../style'; -class MockField extends AbstractField { +export class MockField extends AbstractField { + private readonly _dataType: string; + private readonly _supportsAutoDomain: boolean; + + constructor({ + fieldName, + origin = FIELD_ORIGIN.SOURCE, + dataType = 'string', + supportsAutoDomain = true, + }: { + fieldName: string; + origin?: FIELD_ORIGIN; + dataType?: string; + supportsAutoDomain?: boolean; + }) { + super({ fieldName, origin }); + this._dataType = dataType; + this._supportsAutoDomain = supportsAutoDomain; + } + async getLabel(): Promise { return this.getName() + '_label'; } + + async getDataType(): Promise { + return this._dataType; + } + + supportsAutoDomain(): boolean { + return this._supportsAutoDomain; + } + supportsFieldMeta(): boolean { return true; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_fields_helper.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_fields_helper.ts new file mode 100644 index 0000000000000..8613f9e1e946f --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_fields_helper.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CATEGORICAL_DATA_TYPES, + FIELD_ORIGIN, + ORDINAL_DATA_TYPES, + VECTOR_STYLES, +} from '../../../../common/constants'; +import { IField } from '../../fields/field'; + +export interface StyleField { + label: string; + name: string; + origin: FIELD_ORIGIN; + type: string; + supportsAutoDomain: boolean; +} + +export async function createStyleFieldsHelper(fields: IField[]): Promise { + const promises: Array> = fields.map(async (field: IField) => { + return { + label: await field.getLabel(), + name: field.getName(), + origin: field.getOrigin(), + type: await field.getDataType(), + supportsAutoDomain: field.supportsAutoDomain(), + }; + }); + const styleFields = await Promise.all(promises); + return new StyleFieldsHelper(styleFields); +} + +class StyleFieldsHelper { + private readonly _styleFields: StyleField[]; + private readonly _ordinalAndCategoricalFields: StyleField[]; + private readonly _numberFields: StyleField[]; + private readonly _ordinalFields: StyleField[]; + + constructor(styleFields: StyleField[]) { + const ordinalAndCategoricalFields: StyleField[] = []; + const numberFields: StyleField[] = []; + const ordinalFields: StyleField[] = []; + + styleFields.forEach((styleField: StyleField) => { + if ( + CATEGORICAL_DATA_TYPES.includes(styleField.type) || + ORDINAL_DATA_TYPES.includes(styleField.type) + ) { + ordinalAndCategoricalFields.push(styleField); + } + + if (styleField.type === 'date' || styleField.type === 'number') { + if (styleField.type === 'number') { + numberFields.push(styleField); + } + if (styleField.supportsAutoDomain) { + ordinalFields.push(styleField); + } + } + }); + + this._styleFields = styleFields; + this._ordinalAndCategoricalFields = ordinalAndCategoricalFields; + this._numberFields = numberFields; + this._ordinalFields = ordinalFields; + } + + getFieldsForStyle(styleName: VECTOR_STYLES): StyleField[] { + switch (styleName) { + case VECTOR_STYLES.ICON_ORIENTATION: + return this._numberFields; + case VECTOR_STYLES.FILL_COLOR: + case VECTOR_STYLES.LINE_COLOR: + case VECTOR_STYLES.LABEL_COLOR: + case VECTOR_STYLES.LABEL_BORDER_COLOR: + case VECTOR_STYLES.ICON: + return this._ordinalAndCategoricalFields; + case VECTOR_STYLES.LINE_WIDTH: + case VECTOR_STYLES.LABEL_SIZE: + case VECTOR_STYLES.ICON_SIZE: + return this._ordinalFields; + case VECTOR_STYLES.LABEL_TEXT: + return this._styleFields; + default: + return []; + } + } + + getStyleFields(): StyleField[] { + return this._styleFields; + } +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js index 28801a402ca14..1dbadc054c8a0 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js @@ -12,23 +12,10 @@ import { VECTOR_SHAPE_TYPE, VECTOR_STYLES, } from '../../../../common/constants'; +import { MockField } from './properties/__tests__/test_util'; jest.mock('../../../kibana_services'); -class MockField { - constructor({ fieldName }) { - this._fieldName = fieldName; - } - - getName() { - return this._fieldName; - } - - isValid() { - return !!this._fieldName; - } -} - class MockSource { constructor({ supportedShapeTypes } = {}) { this._supportedShapeTypes = supportedShapeTypes || Object.values(VECTOR_SHAPE_TYPE); @@ -77,25 +64,25 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { }); }); - it('Should return no changes when next ordinal fields contain existing style property fields', () => { + it('Should return no changes when next ordinal fields contain existing style property fields', async () => { const vectorStyle = new VectorStyle({ properties }, new MockSource()); - const nextFields = [new MockField({ fieldName })]; - const { hasChanges } = vectorStyle.getDescriptorWithMissingStylePropsRemoved( + const nextFields = [new MockField({ fieldName, dataType: 'number' })]; + const { hasChanges } = await vectorStyle.getDescriptorWithMissingStylePropsRemoved( nextFields, mapColors ); expect(hasChanges).toBe(false); }); - it('Should clear missing fields when next ordinal fields do not contain existing style property fields', () => { + it('Should clear missing fields when next ordinal fields do not contain existing style property fields', async () => { const vectorStyle = new VectorStyle({ properties }, new MockSource()); - const nextFields = [new MockField({ fieldName: 'someOtherField' })]; + const nextFields = [new MockField({ fieldName: 'someOtherField', dataType: 'number' })]; const { hasChanges, nextStyleDescriptor, - } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors); + } = await vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors); expect(hasChanges).toBe(true); expect(nextStyleDescriptor.properties[VECTOR_STYLES.LINE_COLOR]).toEqual({ options: {}, @@ -110,14 +97,14 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { }); }); - it('Should convert dynamic styles to static styles when there are no next fields', () => { + it('Should convert dynamic styles to static styles when there are no next fields', async () => { const vectorStyle = new VectorStyle({ properties }, new MockSource()); const nextFields = []; const { hasChanges, nextStyleDescriptor, - } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors); + } = await vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors); expect(hasChanges).toBe(true); expect(nextStyleDescriptor.properties[VECTOR_STYLES.LINE_COLOR]).toEqual({ options: { @@ -132,6 +119,25 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { type: 'STATIC', }); }); + + it('Should convert dynamic ICON_SIZE static style when there are no next ordinal fields', async () => { + const vectorStyle = new VectorStyle({ properties }, new MockSource()); + + const nextFields = [ + new MockField({ fieldName, dataType: 'number', supportsAutoDomain: false }), + ]; + const { + hasChanges, + nextStyleDescriptor, + } = await vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors); + expect(hasChanges).toBe(true); + expect(nextStyleDescriptor.properties[VECTOR_STYLES.ICON_SIZE]).toEqual({ + options: { + size: 6, + }, + type: 'STATIC', + }); + }); }); describe('pluckStyleMetaFromSourceDataRequest', () => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index acb158636e0b3..5d360f5452d36 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -77,6 +77,7 @@ import { IField } from '../../fields/field'; import { IVectorLayer } from '../../layers/vector_layer/vector_layer'; import { IVectorSource } from '../../sources/vector_source'; import { ILayer } from '../../layers/layer'; +import { createStyleFieldsHelper } from './style_fields_helper'; const POINTS = [GEO_JSON_TYPE.POINT, GEO_JSON_TYPE.MULTI_POINT]; const LINES = [GEO_JSON_TYPE.LINE_STRING, GEO_JSON_TYPE.MULTI_LINE_STRING]; @@ -90,7 +91,7 @@ export interface IVectorStyle extends IStyle { getDescriptorWithMissingStylePropsRemoved( nextFields: IField[], mapColors: string[] - ): { hasChanges: boolean; nextStyleDescriptor?: VectorStyleDescriptor }; + ): Promise<{ hasChanges: boolean; nextStyleDescriptor?: VectorStyleDescriptor }>; pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): Promise; isTimeAware: () => boolean; getIcon: () => ReactElement; @@ -324,7 +325,8 @@ export class VectorStyle implements IVectorStyle { * This method does not update its descriptor. It just returns a new descriptor that the caller * can then use to update store state via dispatch. */ - getDescriptorWithMissingStylePropsRemoved(nextFields: IField[], mapColors: string[]) { + async getDescriptorWithMissingStylePropsRemoved(nextFields: IField[], mapColors: string[]) { + const styleFieldsHelper = await createStyleFieldsHelper(nextFields); const originalProperties = this.getRawProperties(); const updatedProperties = {} as VectorStylePropertiesDescriptor; @@ -346,8 +348,9 @@ export class VectorStyle implements IVectorStyle { }); dynamicProperties.forEach((key: VECTOR_STYLES) => { - // Convert dynamic styling to static stying when there are no nextFields - if (nextFields.length === 0) { + // Convert dynamic styling to static stying when there are no style fields + const styleFields = styleFieldsHelper.getFieldsForStyle(key); + if (styleFields.length === 0) { const staticProperties = getDefaultStaticProperties(mapColors); updatedProperties[key] = staticProperties[key] as any; return; diff --git a/x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property.ts index fad6d6b84f77f..efdede82a7449 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property.ts @@ -40,7 +40,8 @@ export class JoinTooltipProperty implements ITooltipProperty { async getESFilters(): Promise { const esFilters = []; if (this._tooltipProperty.isFilterable()) { - esFilters.push(...(await this._tooltipProperty.getESFilters())); + const filters = await this._tooltipProperty.getESFilters(); + esFilters.push(...filters); } for (let i = 0; i < this._leftInnerJoins.length; i++) { @@ -51,7 +52,8 @@ export class JoinTooltipProperty implements ITooltipProperty { this._tooltipProperty.getRawValue() ); if (esTooltipProperty) { - esFilters.push(...(await esTooltipProperty.getESFilters())); + const filters = await esTooltipProperty.getESFilters(); + esFilters.push(...filters); } } catch (e) { // eslint-disable-next-line no-console diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index 782d645dc230a..067f213603fe3 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -15,7 +15,7 @@ export * from '../../actions'; export * from '../../selectors/map_selectors'; export * from '../../routing/bootstrap/get_initial_layers'; export * from '../../embeddable/merge_input_with_saved_map'; -export * from '../../routing/maps_router'; +export { renderApp } from '../../routing/render_app'; export * from '../../classes/layers/solution_layers/security'; export { registerLayerWizard } from '../../classes/layers/layer_wizard_registry'; export { registerSource } from '../../classes/sources/source_registry'; diff --git a/x-pack/plugins/maps/public/routing/maps_router.tsx b/x-pack/plugins/maps/public/routing/render_app.tsx similarity index 81% rename from x-pack/plugins/maps/public/routing/maps_router.tsx rename to x-pack/plugins/maps/public/routing/render_app.tsx index d7e6e6e079953..65cccc53f5047 100644 --- a/x-pack/plugins/maps/public/routing/maps_router.tsx +++ b/x-pack/plugins/maps/public/routing/render_app.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Router, Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom'; +import { Router, Switch, Route, Redirect } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { Provider } from 'react-redux'; import { AppMountParameters } from 'kibana/public'; @@ -24,13 +24,12 @@ import { } from '../../../../../src/plugins/kibana_utils/public'; import { getStore } from './store_operations'; import { LoadListAndRender } from './routes/list/load_list_and_render'; -import { LoadMapAndRender } from './routes/maps_app/load_map_and_render'; +import { MapApp } from './routes/map_app'; export let goToSpecifiedPath: (path: string) => void; export let kbnUrlStateStorage: IKbnUrlStateStorage; export async function renderApp({ - appBasePath, element, history, onAppLeave, @@ -43,29 +42,6 @@ export async function renderApp({ ...withNotifyOnErrors(getToasts()), }); - render( - , - element - ); - - return () => { - unmountComponentAtNode(element); - }; -} - -interface Props { - history: AppMountParameters['history'] | RouteComponentProps['history']; - appBasePath: AppMountParameters['appBasePath']; - onAppLeave: AppMountParameters['onAppLeave']; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; -} - -const App: React.FC = ({ history, appBasePath, onAppLeave, setHeaderActionMenu }) => { const store = getStore(); const I18nContext = getCoreI18n().Context; @@ -88,7 +64,7 @@ const App: React.FC = ({ history, appBasePath, onAppLeave, setHeaderActio }); } - return ( + render( @@ -96,7 +72,7 @@ const App: React.FC = ({ history, appBasePath, onAppLeave, setHeaderActio ( - = ({ history, appBasePath, onAppLeave, setHeaderActio exact path={`/map`} render={() => ( - = ({ history, appBasePath, onAppLeave, setHeaderActio - + , + element ); -}; + + return () => { + unmountComponentAtNode(element); + }; +} diff --git a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx index 3b0891a4fd44d..ca92442ae93e6 100644 --- a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.tsx @@ -30,7 +30,7 @@ import { EuiBasicTableColumn, } from '@elastic/eui/src/components/basic_table/basic_table'; import { EuiTableSortingType } from '@elastic/eui'; -import { goToSpecifiedPath } from '../../maps_router'; +import { goToSpecifiedPath } from '../../render_app'; // @ts-expect-error import { addHelpMenuToAppChrome } from '../../../help_menu_util'; import { APP_ID, MAP_PATH } from '../../../../common/constants'; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx b/x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.test.tsx similarity index 96% rename from x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx rename to x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.test.tsx index e8e0e583a7c6d..3516daf526968 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.test.tsx +++ b/x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.test.tsx @@ -7,7 +7,7 @@ import { getBreadcrumbs } from './get_breadcrumbs'; jest.mock('../../../kibana_services', () => {}); -jest.mock('../../maps_router', () => {}); +jest.mock('../../render_app', () => {}); const getHasUnsavedChanges = () => { return false; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.tsx similarity index 96% rename from x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx rename to x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.tsx index d9b60b670b93e..88dba0f83ec2f 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx +++ b/x-pack/plugins/maps/public/routing/routes/map_app/get_breadcrumbs.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { getCoreOverlays, getNavigateToApp } from '../../../kibana_services'; -import { goToSpecifiedPath } from '../../maps_router'; +import { goToSpecifiedPath } from '../../render_app'; import { getAppTitle } from '../../../../common/i18n_getters'; export const unsavedChangesWarning = i18n.translate( diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts b/x-pack/plugins/maps/public/routing/routes/map_app/index.ts similarity index 94% rename from x-pack/plugins/maps/public/routing/routes/maps_app/index.ts rename to x-pack/plugins/maps/public/routing/routes/map_app/index.ts index 812d7fcf30981..0b9f0cfe33e44 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts +++ b/x-pack/plugins/maps/public/routing/routes/map_app/index.ts @@ -8,7 +8,7 @@ import { connect } from 'react-redux'; import { ThunkDispatch } from 'redux-thunk'; import { AnyAction } from 'redux'; import { Filter, Query, TimeRange } from 'src/plugins/data/public'; -import { MapsAppView } from './maps_app_view'; +import { MapApp } from './map_app'; import { getFlyoutDisplay, getIsFullScreen } from '../../../selectors/ui_selectors'; import { getFilters, @@ -99,5 +99,5 @@ function mapDispatchToProps(dispatch: ThunkDispatch { +export class MapApp extends React.Component { _globalSyncUnsubscribe: (() => void) | null = null; _globalSyncChangeMonitorSubscription: Subscription | null = null; _appSyncUnsubscribe: (() => void) | null = null; @@ -134,8 +139,6 @@ export class MapsAppView extends React.Component { this._initMap(); - this._setBreadcrumbs(); - this.props.onAppLeave((actions) => { if (this._hasUnsavedChanges()) { return actions.confirm(unsavedChangesWarning, unsavedChangesTitle); @@ -165,7 +168,11 @@ export class MapsAppView extends React.Component { } _hasUnsavedChanges = () => { - const savedLayerList = this.props.savedMap.getLayerList(); + if (!this.state.savedMap) { + return false; + } + + const savedLayerList = this.state.savedMap.getLayerList(); return !savedLayerList ? !_.isEqual(this.props.layerListConfigOnly, this.state.initialLayerListConfig) : // savedMap stores layerList as a JSON string using JSON.stringify. @@ -176,9 +183,9 @@ export class MapsAppView extends React.Component { !_.isEqual(JSON.parse(JSON.stringify(this.props.layerListConfigOnly)), savedLayerList); }; - _setBreadcrumbs = () => { + _setBreadcrumbs = (title: string) => { const breadcrumbs = getBreadcrumbs({ - title: this.props.savedMap.title, + title, getHasUnsavedChanges: this._hasUnsavedChanges, originatingApp: this.state.originatingApp, getAppNameFromId: this.props.stateTransfer.getAppNameFromId, @@ -255,13 +262,12 @@ export class MapsAppView extends React.Component { updateGlobalState(updatedGlobalState, !this.state.initialized); }; - _initMapAndLayerSettings() { + _initMapAndLayerSettings(savedMap: ISavedGisMap) { const globalState: MapsGlobalState = getGlobalState(); - const mapStateJSON = this.props.savedMap.mapStateJSON; let savedObjectFilters = []; - if (mapStateJSON) { - const mapState = JSON.parse(mapStateJSON); + if (savedMap.mapStateJSON) { + const mapState = JSON.parse(savedMap.mapStateJSON); if (mapState.filters) { savedObjectFilters = mapState.filters; } @@ -269,7 +275,7 @@ export class MapsAppView extends React.Component { const appFilters = this._appStateManager.getFilters() || []; const query = getInitialQuery({ - mapStateJSON, + mapStateJSON: savedMap.mapStateJSON, appState: this._appStateManager.getAppState(), }); if (query) { @@ -280,22 +286,19 @@ export class MapsAppView extends React.Component { filters: [..._.get(globalState, 'filters', []), ...appFilters, ...savedObjectFilters], query, time: getInitialTimeFilters({ - mapStateJSON, + mapStateJSON: savedMap.mapStateJSON, globalState, }), }); this._onRefreshConfigChange( getInitialRefreshConfig({ - mapStateJSON, + mapStateJSON: savedMap.mapStateJSON, globalState, }) ); - const layerList = getInitialLayers( - this.props.savedMap.layerListJSON, - getInitialLayersFromUrlParam() - ); + const layerList = getInitialLayers(savedMap.layerListJSON, getInitialLayersFromUrlParam()); this.props.replaceLayerList(layerList); this.setState({ initialLayerListConfig: copyPersistentState(layerList), @@ -345,13 +348,43 @@ export class MapsAppView extends React.Component { }); }; - _initMap() { - this._initMapAndLayerSettings(); + async _loadSavedMap(): Promise { + let savedMap: ISavedGisMap | null = null; + try { + savedMap = await getMapsSavedObjectLoader().get(this.props.savedMapId); + } catch (err) { + if (this._isMounted) { + getToasts().addWarning({ + title: i18n.translate('xpack.maps.loadMap.errorAttemptingToLoadSavedMap', { + defaultMessage: `Unable to load map`, + }), + text: `${err.message}`, + }); + goToSpecifiedPath('/'); + } + } + + return savedMap; + } + + async _initMap() { + const savedMap = await this._loadSavedMap(); + if (!this._isMounted || !savedMap) { + return; + } + + this._setBreadcrumbs(savedMap.title); + getCoreChrome().docTitle.change(savedMap.title); + if (this.props.savedMapId) { + getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id!); + } + + this._initMapAndLayerSettings(savedMap); this.props.clearUi(); - if (this.props.savedMap.mapStateJSON) { - const mapState = JSON.parse(this.props.savedMap.mapStateJSON); + if (savedMap.mapStateJSON) { + const mapState = JSON.parse(savedMap.mapStateJSON); this.props.setGotoWithCenter({ lat: mapState.center.lat, lon: mapState.center.lon, @@ -362,22 +395,22 @@ export class MapsAppView extends React.Component { } } - if (this.props.savedMap.uiStateJSON) { - const uiState = JSON.parse(this.props.savedMap.uiStateJSON); + if (savedMap.uiStateJSON) { + const uiState = JSON.parse(savedMap.uiStateJSON); this.props.setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN)); this.props.setOpenTOCDetails(_.get(uiState, 'openTOCDetails', [])); } - this.setState({ initialized: true }); + this.setState({ initialized: true, savedMap }); } _renderTopNav() { - if (this.props.isFullScreen) { + if (this.props.isFullScreen || !this.state.savedMap) { return null; } const topNavConfig = getTopNavConfig({ - savedMap: this.props.savedMap, + savedMap: this.state.savedMap, isOpenSettingsDisabled: this.props.isOpenSettingsDisabled, isSaveDisabled: this.props.isSaveDisabled, enableFullScreen: this.props.enableFullScreen, @@ -452,18 +485,22 @@ export class MapsAppView extends React.Component { }; render() { - return this.state.initialized ? ( + if (!this.state.initialized || !this.state.savedMap) { + return null; + } + + return (
    {this._renderTopNav()}

    {`screenTitle placeholder`}

    - ) : null; + ); } } diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx b/x-pack/plugins/maps/public/routing/routes/map_app/top_nav_config.tsx similarity index 98% rename from x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx rename to x-pack/plugins/maps/public/routing/routes/map_app/top_nav_config.tsx index 917abebfb6b25..c60f44093541f 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routing/routes/map_app/top_nav_config.tsx @@ -21,7 +21,7 @@ import { showSaveModal, } from '../../../../../../../src/plugins/saved_objects/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; -import { goToSpecifiedPath } from '../../maps_router'; +import { goToSpecifiedPath } from '../../render_app'; import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; import { EmbeddableStateTransfer } from '../../../../../../../src/plugins/embeddable/public'; @@ -43,7 +43,7 @@ export function getTopNavConfig({ enableFullScreen: () => void; openMapSettings: () => void; inspectorAdapters: Adapters; - setBreadcrumbs: () => void; + setBreadcrumbs: (title: string) => void; stateTransfer?: EmbeddableStateTransfer; originatingApp?: string; cutOriginatingAppConnection: () => void; @@ -104,7 +104,7 @@ export function getTopNavConfig({ }); getCoreChrome().docTitle.change(savedMap.title); - setBreadcrumbs(); + setBreadcrumbs(savedMap.title); goToSpecifiedPath(`/map/${savedObjectId}${window.location.hash}`); const newlyCreated = newCopyOnSave || isNewMap; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx deleted file mode 100644 index b980756daad20..0000000000000 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { Redirect } from 'react-router-dom'; -import { AppMountParameters } from 'kibana/public'; -import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; -import { getCoreChrome, getToasts } from '../../../kibana_services'; -import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; -import { MapsAppView } from '.'; -import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; - -interface Props { - savedMapId?: string; - onAppLeave: AppMountParameters['onAppLeave']; - stateTransfer: EmbeddableStateTransfer; - originatingApp?: string; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; -} - -interface State { - savedMap?: ISavedGisMap; - failedToLoad: boolean; -} - -export const LoadMapAndRender = class extends React.Component { - _isMounted: boolean = false; - state: State = { - savedMap: undefined, - failedToLoad: false, - }; - - componentDidMount() { - this._isMounted = true; - this._loadSavedMap(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - async _loadSavedMap() { - try { - const savedMap = await getMapsSavedObjectLoader().get(this.props.savedMapId); - if (this._isMounted) { - getCoreChrome().docTitle.change(savedMap.title); - if (this.props.savedMapId) { - getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); - } - this.setState({ savedMap }); - } - } catch (err) { - if (this._isMounted) { - this.setState({ failedToLoad: true }); - getToasts().addWarning({ - title: i18n.translate('xpack.maps.loadMap.errorAttemptingToLoadSavedMap', { - defaultMessage: `Unable to load map`, - }), - text: `${err.message}`, - }); - } - } - } - - render() { - const { savedMap, failedToLoad } = this.state; - - if (failedToLoad) { - return ; - } - - return savedMap ? ( - - ) : null; - } -}; diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts index b346822913bec..498442040681c 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts @@ -8,7 +8,7 @@ import { map } from 'rxjs/operators'; import { connectToQueryState, esFilters } from '../../../../../../src/plugins/data/public'; import { syncState, BaseStateContainer } from '../../../../../../src/plugins/kibana_utils/public'; import { getData } from '../../kibana_services'; -import { kbnUrlStateStorage } from '../maps_router'; +import { kbnUrlStateStorage } from '../render_app'; import { AppStateManager } from './app_state_manager'; export function startAppStateSyncing(appStateManager: AppStateManager) { diff --git a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts index 1e779831c5e0c..3f370d9aa99b2 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts +++ b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts @@ -6,7 +6,7 @@ import { TimeRange, RefreshInterval, Filter } from 'src/plugins/data/public'; import { syncQueryStateWithUrl } from '../../../../../../src/plugins/data/public'; import { getData } from '../../kibana_services'; -import { kbnUrlStateStorage } from '../maps_router'; +import { kbnUrlStateStorage } from '../render_app'; export interface MapsGlobalState { time?: TimeRange; diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index 6b19103b59722..5feacfee4d4d2 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -189,18 +189,28 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) { return badRequest('map.proxyElasticMapsServiceInMaps disabled'); } - const file = await emsClient.getDefaultFileManifest(); - const layers = file.layers.map((layer) => { - const newLayer = { ...layer }; - const id = encodeURIComponent(layer.layer_id); + const file = await emsClient.getDefaultFileManifest(); //need raw manifest + const fileLayers = await emsClient.getFileLayers(); + + const layers = file.layers.map((layerJson) => { + const newLayerJson = { ...layerJson }; + const id = encodeURIComponent(layerJson.layer_id); + + const fileLayer = fileLayers.find((fileLayer) => fileLayer.getId() === layerJson.layer_id); + const defaultFormat = layerJson.formats.find( + (format) => format.type === fileLayer.getDefaultFormatType() + ); + const newUrl = `${EMS_FILES_DEFAULT_JSON_PATH}?id=${id}`; - newLayer.formats = [ + + //Only proxy default-format. Others are unused in Maps-app + newLayerJson.formats = [ { - ...layer.formats[0], + ...defaultFormat, url: newUrl, }, ]; - return newLayer; + return newLayerJson; }); //rewrite return ok({ diff --git a/x-pack/plugins/ml/common/types/feature_importance.ts b/x-pack/plugins/ml/common/types/feature_importance.ts index 4f5619cf3ab7b..1ae4c7832390c 100644 --- a/x-pack/plugins/ml/common/types/feature_importance.ts +++ b/x-pack/plugins/ml/common/types/feature_importance.ts @@ -8,10 +8,13 @@ export interface ClassFeatureImportance { class_name: string | boolean; importance: number; } + +// TODO We should separate the interface because classes/importance +// isn't both optional but either/or. export interface FeatureImportance { feature_name: string; - importance?: number; classes?: ClassFeatureImportance[]; + importance?: number; } export interface TopClass { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts index 4bb670ad02dfc..aaf6f90b00f4d 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts @@ -8,7 +8,7 @@ import { EuiDataGridSorting } from '@elastic/eui'; import { multiColumnSortFactory } from './common'; -describe('Transform: Define Pivot Common', () => { +describe('Data Frame Analytics: Data Grid Common', () => { test('multiColumnSortFactory()', () => { const data = [ { s: 'a', n: 1 }, diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 642d0ae564b85..48a0a0c9ab126 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -24,7 +24,9 @@ import { KBN_FIELD_TYPES, } from '../../../../../../../src/plugins/data/public'; +import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; import { extractErrorMessage } from '../../../../common/util/errors'; +import { FeatureImportance, TopClasses } from '../../../../common/types/feature_importance'; import { BASIC_NUMERICAL_TYPES, @@ -158,6 +160,90 @@ export const getDataGridSchemaFromKibanaFieldType = ( return schema; }; +const getClassName = (className: string, isClassTypeBoolean: boolean) => { + if (isClassTypeBoolean) { + return className === 'true'; + } + + return className; +}; +/** + * Helper to transform feature importance flattened fields with arrays back to object structure + * + * @param row - EUI data grid data row + * @param mlResultsField - Data frame analytics results field + * @returns nested object structure of feature importance values + */ +export const getFeatureImportance = ( + row: Record, + mlResultsField: string, + isClassTypeBoolean = false +): FeatureImportance[] => { + const featureNames: string[] | undefined = + row[`${mlResultsField}.feature_importance.feature_name`]; + const classNames: string[] | undefined = + row[`${mlResultsField}.feature_importance.classes.class_name`]; + const classImportance: number[] | undefined = + row[`${mlResultsField}.feature_importance.classes.importance`]; + + if (featureNames === undefined) { + return []; + } + + // return object structure for classification job + if (classNames !== undefined && classImportance !== undefined) { + const overallClassNames = classNames?.slice(0, classNames.length / featureNames.length); + + return featureNames.map((fName, index) => { + const offset = overallClassNames.length * index; + const featureClassImportance = classImportance.slice( + offset, + offset + overallClassNames.length + ); + return { + feature_name: fName, + classes: overallClassNames.map((fClassName, fIndex) => { + return { + class_name: getClassName(fClassName, isClassTypeBoolean), + importance: featureClassImportance[fIndex], + }; + }), + }; + }); + } + + // return object structure for regression job + const importance: number[] = row[`${mlResultsField}.feature_importance.importance`]; + return featureNames.map((fName, index) => ({ + feature_name: fName, + importance: importance[index], + })); +}; + +/** + * Helper to transforms top classes flattened fields with arrays back to object structure + * + * @param row - EUI data grid data row + * @param mlResultsField - Data frame analytics results field + * @returns nested object structure of feature importance values + */ +export const getTopClasses = (row: Record, mlResultsField: string): TopClasses => { + const classNames: string[] | undefined = row[`${mlResultsField}.top_classes.class_name`]; + const classProbabilities: number[] | undefined = + row[`${mlResultsField}.top_classes.class_probability`]; + const classScores: number[] | undefined = row[`${mlResultsField}.top_classes.class_score`]; + + if (classNames === undefined || classProbabilities === undefined || classScores === undefined) { + return []; + } + + return classNames.map((className, index) => ({ + class_name: className, + class_probability: classProbabilities[index], + class_score: classScores[index], + })); +}; + export const useRenderCellValue = ( indexPattern: IndexPattern | undefined, pagination: IndexPagination, @@ -207,6 +293,15 @@ export const useRenderCellValue = ( return item[cId]; } + // For classification and regression results, we need to treat some fields with a custom transform. + if (cId === `${resultsField}.feature_importance`) { + return getFeatureImportance(fullItem, resultsField ?? DEFAULT_RESULTS_FIELD); + } + + if (cId === `${resultsField}.top_classes`) { + return getTopClasses(fullItem, resultsField ?? DEFAULT_RESULTS_FIELD); + } + // Try if the field name is available as a nested field. return getNestedProperty(tableItems[adjustedRowIndex], cId, null); } diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index fad2439f5d5ee..50e9cabc99c35 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -27,10 +27,15 @@ import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../common/constants/field_h import { ANALYSIS_CONFIG_TYPE, INDEX_STATUS } from '../../data_frame_analytics/common'; -import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; +import { + euiDataGridStyle, + euiDataGridToolbarSettings, + getFeatureImportance, + getTopClasses, +} from './common'; import { UseIndexDataReturnType } from './types'; import { DecisionPathPopover } from './feature_importance/decision_path_popover'; -import { TopClasses } from '../../../../common/types/feature_importance'; +import { FeatureImportance, TopClasses } from '../../../../common/types/feature_importance'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; import { DataFrameAnalysisConfigType } from '../../../../common/types/data_frame_analytics'; @@ -118,18 +123,28 @@ export const DataGrid: FC = memo( if (!row) return
    ; // if resultsField for some reason is not available then use ml const mlResultsField = resultsField ?? DEFAULT_RESULTS_FIELD; - const parsedFIArray = row[mlResultsField].feature_importance; let predictedValue: string | number | undefined; let topClasses: TopClasses = []; if ( predictionFieldName !== undefined && row && - row[mlResultsField][predictionFieldName] !== undefined + row[`${mlResultsField}.${predictionFieldName}`] !== undefined ) { - predictedValue = row[mlResultsField][predictionFieldName]; - topClasses = row[mlResultsField].top_classes; + predictedValue = row[`${mlResultsField}.${predictionFieldName}`]; + topClasses = getTopClasses(row, mlResultsField); } + const isClassTypeBoolean = topClasses.reduce( + (p, c) => typeof c.class_name === 'boolean' || p, + false + ); + + const parsedFIArray: FeatureImportance[] = getFeatureImportance( + row, + mlResultsField, + isClassTypeBoolean + ); + return ( !field.name.includes(`${resultsField}.${FEATURE_IMPORTANCE}.`) + ); } if ((numTopClasses ?? 0) > 0) { @@ -221,6 +225,10 @@ export const getDefaultFieldsFromJobCaps = ( name: `${resultsField}.${TOP_CLASSES}`, type: KBN_FIELD_TYPES.UNKNOWN, }); + // remove flattened top classes fields + fields = fields.filter( + (field: any) => !field.name.includes(`${resultsField}.${TOP_CLASSES}.`) + ); } // Only need to add these fields if we didn't use dest index pattern to get the fields diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts index 8e50aab0914db..85f222109d408 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -53,7 +53,7 @@ export const getIndexData = async ( index: jobConfig.dest.index, body: { fields: ['*'], - _source: [], + _source: false, query: searchQuery, from: pageIndex * pageSize, size: pageSize, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index a6e95269b3633..10e2ad5b5eb53 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -29,7 +29,7 @@ interface Props { } export const ExplorationResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, needsDestIndexPattern, searchQuery }) => { + ({ indexPattern, jobConfig, needsDestIndexPattern, searchQuery }) => { const { services: { mlServices: { mlApiServices }, diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index e0f1b01dafa13..48aed19ea9050 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -456,16 +456,10 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat }, mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { id: string; force?: boolean | undefined } = { + const { body } = await client.asInternalUser.ml.stopDataFrameAnalytics({ id: request.params.analyticsId, - }; - // @ts-expect-error TODO: update types - if (request.url?.query?.force !== undefined) { - // @ts-expect-error TODO: update types - options.force = request.url.query.force; - } - - const { body } = await client.asInternalUser.ml.stopDataFrameAnalytics(options); + force: request.query.force, + }); return response.ok({ body, }); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts b/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts index e61564f9df753..017f385d36735 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts +++ b/x-pack/plugins/observability/public/components/app/section/ux/mock_data/ux.mock.ts @@ -14,6 +14,7 @@ export const response: UxFetchDataResponse = { lcp: 1942.6666666666667, tbt: 281.55833333333334, fcp: 1487, + coreVitalPages: 100, lcpRanks: [65, 19, 16], fidRanks: [73, 11, 16], clsRanks: [86, 8, 6], diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx index 0d0a388855ff2..18831565b8784 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/core_vital_item.tsx @@ -34,7 +34,7 @@ export interface Thresholds { interface Props { title: string; - value?: string; + value?: string | null; ranks?: number[]; loading: boolean; thresholds: Thresholds; @@ -88,14 +88,14 @@ export function CoreVitalItem({ const biggestValIndex = ranks.indexOf(Math.max(...ranks)); - if (value === undefined && ranks[0] === 100 && !loading) { + if ((value === null || value !== undefined) && ranks[0] === 100 && !loading) { return ; } return ( <> {title} diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx index 6a507176e55f6..f5683310c3b7c 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx @@ -18,11 +18,12 @@ import { WebCoreVitalsTitle } from './web_core_vitals_title'; import { ServiceName } from './service_name'; export interface UXMetrics { - cls: string; - fid: number; - lcp: number; + cls: string | null; + fid?: number | null; + lcp?: number | null; tbt: number; - fcp: number; + fcp?: number | null; + coreVitalPages: number; lcpRanks: number[]; fidRanks: number[]; clsRanks: number[]; @@ -48,21 +49,35 @@ interface Props { data?: UXMetrics | null; displayServiceName?: boolean; serviceName?: string; + totalPageViews?: number; + displayTrafficMetric?: boolean; } -function formatValue(value?: number) { - if (typeof value === 'undefined') { - return undefined; +function formatValue(value?: number | null) { + if (typeof value === 'undefined' || value === null) { + return null; } return formatToSec(value, 'ms'); } -export function CoreVitals({ data, loading, displayServiceName, serviceName }: Props) { - const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks } = data || {}; +export function CoreVitals({ + data, + loading, + displayServiceName, + serviceName, + totalPageViews, + displayTrafficMetric = false, +}: Props) { + const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks, coreVitalPages } = data || {}; return ( <> - + {displayServiceName && } @@ -90,7 +105,7 @@ export function CoreVitals({ data, loading, displayServiceName, serviceName }: P setIsPopoverOpen(false); + const closeBrowserPopover = () => setIsBrowserPopoverOpen(false); return ( - -

    - {CORE_WEB_VITALS} - setIsPopoverOpen(true)} - color={'text'} - iconType={'questionInCircle'} - /> - } - closePopover={closePopover} - > -
    - + + + +

    + {CORE_WEB_VITALS} + setIsPopoverOpen(true)} + color={'text'} + iconType={'questionInCircle'} + /> + } + closePopover={closePopover} + > +
    + + {' '} + + {CORE_WEB_VITALS} + + +
    +
    +

    +
    +
    + {displayTrafficMetric && totalPageViews > 0 && ( + + {loading ? ( + + ) : ( + {(((coreVitalPages || 0) / totalPageViews) * 100).toFixed(0)}% + ), + }} /> - - {' '} - {CORE_WEB_VITALS} - + + setIsBrowserPopoverOpen(true)} + color={'text'} + iconType={'questionInCircle'} + /> + } + closePopover={closeBrowserPopover} + > +
    + + {' '} + + {BROWSER_CORE_WEB_VITALS} + + +
    +
    -
    -
    -

    -
    + )} +
    + )} + ); } diff --git a/x-pack/plugins/observability/public/data_handler.test.ts b/x-pack/plugins/observability/public/data_handler.test.ts index dae2f62777d30..8fdfc2bc622ca 100644 --- a/x-pack/plugins/observability/public/data_handler.test.ts +++ b/x-pack/plugins/observability/public/data_handler.test.ts @@ -287,6 +287,7 @@ describe('registerDataHandler', () => { lcp: 1464.3333333333333, tbt: 232.92166666666665, fcp: 1154.8, + coreVitalPages: 100, lcpRanks: [73, 16, 11], fidRanks: [85, 4, 11], clsRanks: [88, 7, 5], @@ -314,6 +315,7 @@ describe('registerDataHandler', () => { lcp: 1464.3333333333333, tbt: 232.92166666666665, fcp: 1154.8, + coreVitalPages: 100, lcpRanks: [73, 16, 11], fidRanks: [85, 4, 11], clsRanks: [88, 7, 5], diff --git a/x-pack/plugins/painless_lab/README.md b/x-pack/plugins/painless_lab/README.md new file mode 100644 index 0000000000000..519b6a9dea8ed --- /dev/null +++ b/x-pack/plugins/painless_lab/README.md @@ -0,0 +1,5 @@ +# Painless Lab + +## About + +This plugin helps users learn how to use the [Painless scripting language](https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-scripting-painless.html). \ No newline at end of file diff --git a/x-pack/plugins/remote_clusters/README.md b/x-pack/plugins/remote_clusters/README.md new file mode 100644 index 0000000000000..1119c98ffe84a --- /dev/null +++ b/x-pack/plugins/remote_clusters/README.md @@ -0,0 +1,5 @@ +# Remote Clusters + +## About + +This plugin helps users manage their [remote clusters](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-remote-clusters.html), which enable cross-cluster search and cross-cluster replication. \ No newline at end of file diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 7782576bb5a00..1e3f7e34bebdb 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -108,7 +108,7 @@ export class GetCsvReportPanelAction implements ActionDefinition const filename = embeddable.getSavedSearch().title; const timezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; const fromTime = dateMath.parse(from); - const toTime = dateMath.parse(to); + const toTime = dateMath.parse(to, { roundUp: true }); if (!fromTime || !toTime) { return this.onGenerationFail( diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index abd86d51fb6b6..d62adc62bc9aa 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -210,11 +210,18 @@ export class ReportingCore { } public getFakeRequest(baseRequest: object, spaceId: string | undefined, logger = this.logger) { + // @ts-expect-error _core isn't supposed to be accessed - remove once we upgrade to hapi v18 const fakeRequest = KibanaRequest.from({ path: '/', route: { settings: {} }, url: { href: '/' }, raw: { req: { url: '/' } }, + // TODO: Remove once we upgrade to hapi v18 + _core: { + info: { + uri: 'http://localhost', + }, + }, ...baseRequest, } as Hapi.Request); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index cee8a88000e29..cce002a0e6935 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -27,7 +27,7 @@ const getMockContext = () => const getMockRequest = () => ({ - url: { port: '5601', query: '', path: '/foo' }, + url: { port: '5601', search: '', pathname: '/foo' }, route: { path: '/foo', options: {} }, } as KibanaRequest); diff --git a/x-pack/plugins/searchprofiler/README.md b/x-pack/plugins/searchprofiler/README.md new file mode 100644 index 0000000000000..6e25163470488 --- /dev/null +++ b/x-pack/plugins/searchprofiler/README.md @@ -0,0 +1,8 @@ +# Search Profiler + +## About + +The search profiler consumes the [Profile API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-profile.html) +by sending a `search` API with `profile: true` enabled in the request body. The response contains +detailed information on how Elasticsearch executed the search request. People use this information +to understand why a search request might be slow. \ No newline at end of file diff --git a/x-pack/plugins/security/public/account_management/account_management_page.tsx b/x-pack/plugins/security/public/account_management/account_management_page.tsx index 6615e8fee9412..2c870bf788ceb 100644 --- a/x-pack/plugins/security/public/account_management/account_management_page.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.tsx @@ -6,6 +6,7 @@ import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { CoreStart, NotificationsStart } from 'src/core/public'; import { getUserDisplayName, AuthenticatedUser } from '../../common/model'; import { AuthenticationServiceSetup } from '../authentication'; diff --git a/x-pack/plugins/security/public/account_management/change_password/change_password.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx index 5b27df24f975c..031a70c497469 100644 --- a/x-pack/plugins/security/public/account_management/change_password/change_password.tsx +++ b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx @@ -6,6 +6,7 @@ import React, { Component } from 'react'; import { EuiDescribedFormGroup } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsSetup } from 'src/core/public'; import { AuthenticatedUser, canUserChangePassword } from '../../../common/model'; import { UserAPIClient } from '../../management/users'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index 94f9de010cc2a..192d554bdc873 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -8,6 +8,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ReactWrapper } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotEnabled } from './not_enabled'; import { PermissionDenied } from './permission_denied'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 8621b01299e4f..b4ea91ea024f9 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -26,6 +26,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import moment from 'moment-timezone'; import { ApplicationStart, NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx index 1f47a426e6ae9..3c1e608f97727 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx @@ -6,6 +6,7 @@ import React, { Fragment, useRef, useState } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'src/core/public'; import { ApiKeyToInvalidate } from '../../../../../common/model'; diff --git a/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx index 7abf34700ad7a..49dd83ac1bd26 100644 --- a/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx @@ -7,6 +7,7 @@ import React, { Fragment, useRef, useState, ReactElement } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart } from 'src/core/public'; import { RoleMapping } from '../../../../../common/model'; import { RoleMappingsAPIClient } from '../../role_mappings_api_client'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx index 04dc9c6dfa950..aff8ed12bceb6 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { findTestSubject } from 'test_utils/find_test_subject'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx index b4e3627039ecb..30584348960a5 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart, ScopedHistory } from 'src/core/public'; import { RoleMapping } from '../../../../common/model'; import { RuleEditorPanel } from './rule_editor_panel'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx index 9b62ca27ca569..aaf223f5a8dcb 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from 'test_utils/find_test_subject'; import { Role, RoleMapping } from '../../../../../common/model'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx index 3df9987141fb2..faf0278e5d8f3 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { RoleMapping } from '../../../../../common/model'; import { RolesAPIClient } from '../../../roles'; import { diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.test.tsx index 62f53acb05462..29c5acdb1dda4 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from 'test_utils/find_test_subject'; import { EuiComboBox } from '@elastic/eui'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { RoleSelector } from './role_selector'; import { Role, RoleMapping } from '../../../../../common/model'; import { RoleTemplateEditor } from './role_template_editor'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx index b0f558ee71be8..163637abae1a2 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx @@ -8,6 +8,7 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { RoleMapping, Role, isRoleDeprecated } from '../../../../../common/model'; import { RolesAPIClient } from '../../../roles'; import { AddRoleTemplateButton } from './add_role_template_button'; diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index cf0cb21598c62..98e59eb95f0da 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -24,6 +24,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart, ApplicationStart, ScopedHistory } from 'src/core/public'; import { RoleMapping, Role } from '../../../../common/model'; import { EmptyPrompt } from './empty_prompt'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 650d7c9c86c2b..d24191c54bd94 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -30,6 +30,7 @@ import React, { useRef, useState, } from 'react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Capabilities, FatalErrorsSetup, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx index 46e0183b2f38c..8fc09ce167400 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import React, { Component, Fragment } from 'react'; import { Role, BuiltinESPrivileges } from '../../../../../../common/model'; import { SecurityLicense } from '../../../../../../common/licensing'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx index e4a2bbd260deb..5bc957993dd4a 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx @@ -5,6 +5,7 @@ */ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Role, RoleIndexPrivilege, diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx index d1b266d2a68fe..a6429a87101b3 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart } from 'src/core/public'; import { RolesAPIClient } from '../../roles_api_client'; diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index eb6d25c9ef5ab..1ee621f9ba12d 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -5,6 +5,7 @@ */ import { EuiIcon, EuiBasicTable } from '@elastic/eui'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 1e733dfa0767e..be23aace106df 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -25,6 +25,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart } from 'src/core/public'; import { ScopedHistory } from 'kibana/public'; import { diff --git a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx index 5d889abdf46ce..c7ecdd1bc5564 100644 --- a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -15,6 +15,7 @@ import { import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import React, { ChangeEvent, Component } from 'react'; import { NotificationsStart } from 'src/core/public'; import { User } from '../../../../../common/model'; diff --git a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx index a6fbc6be945c9..5d3e6d40f2c24 100644 --- a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx @@ -8,6 +8,7 @@ import React, { Component, Fragment } from 'react'; import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart } from 'src/core/public'; import { UserAPIClient } from '../..'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index 9a62187cffd33..dc0c3336cb85f 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -30,6 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart, ScopedHistory } from 'src/core/public'; import { User, EditUser, Role, isRoleDeprecated } from '../../../../common/model'; import { AuthenticationServiceSetup } from '../../../authentication'; diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index debe1c5e82d3b..2b7ed0079f291 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart, ApplicationStart, ScopedHistory } from 'src/core/public'; import { User, Role } from '../../../../common/model'; import { ConfirmDeleteUsers } from '../components'; diff --git a/x-pack/plugins/security/server/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts index ae40429eea1b6..f153b9efb9d43 100644 --- a/x-pack/plugins/security/server/audit/audit_events.test.ts +++ b/x-pack/plugins/security/server/audit/audit_events.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { URL } from 'url'; import { EventOutcome, SavedObjectAction, @@ -192,11 +193,48 @@ describe('#httpRequestEvent', () => { }, "message": "User is requesting [/path] endpoint", "url": Object { - "domain": undefined, + "domain": "localhost", "path": "/path", "port": undefined, "query": undefined, - "scheme": undefined, + "scheme": "http:", + }, + } + `); + }); + + test('uses original URL if rewritten', () => { + expect( + httpRequestEvent({ + request: httpServerMock.createKibanaRequest({ + path: '/path', + query: { query: 'param' }, + kibanaRequestState: { + requestId: '123', + requestUuid: '123e4567-e89b-12d3-a456-426614174000', + rewrittenUrl: new URL('http://localhost/original/path?query=param'), + }, + }), + }) + ).toMatchInlineSnapshot(` + Object { + "event": Object { + "action": "http_request", + "category": "web", + "outcome": "unknown", + }, + "http": Object { + "request": Object { + "method": "get", + }, + }, + "message": "User is requesting [/original/path] endpoint", + "url": Object { + "domain": "localhost", + "path": "/original/path", + "port": undefined, + "query": "query=param", + "scheme": "http:", }, } `); diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index 48a3b1e7e85b0..d91c18bf82e02 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -105,10 +105,10 @@ export interface HttpRequestParams { } export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent { - const { pathname, search } = request.url; + const url = request.rewrittenUrl ?? request.url; return { - message: `User is requesting [${pathname}] endpoint`, + message: `User is requesting [${url.pathname}] endpoint`, event: { action: 'http_request', category: EventCategory.WEB, @@ -120,11 +120,11 @@ export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent { }, }, url: { - domain: request.url.hostname, - path: pathname, - port: request.url.port ? parseInt(request.url.port, 10) : undefined, - query: search?.slice(1) || undefined, - scheme: request.url.protocol, + domain: url.hostname, + path: url.pathname, + port: url.port ? parseInt(url.port, 10) : undefined, + query: url.search ? url.search.slice(1) : undefined, + scheme: url.protocol, }, }; } diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index e5bb00cdc056f..51c937f1edcb5 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -10,7 +10,7 @@ jest.mock('./providers/saml'); jest.mock('./providers/http'); import Boom from 'boom'; - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { loggingSystemMock, httpServiceMock, diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 0523ebaffb9d0..80aeb4f8b2959 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { KibanaRequest, LoggerFactory, @@ -333,7 +333,7 @@ export class Authenticator { this.logger.debug('Redirecting request to Login Selector.'); return AuthenticationResult.redirectTo( `${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.path}` + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}` ); } @@ -728,7 +728,7 @@ export class Authenticator { preAccessRedirectURL = `${preAccessRedirectURL}?next=${encodeURIComponent( authenticationResult.redirectURL || redirectURL || - `${this.options.basePath.get(request)}${request.url.path}` + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}`; } else if (redirectURL && !authenticationResult.redirectURL) { preAccessRedirectURL = redirectURL; diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 6f8f17a0a3c74..b5ce5283df8ce 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -8,6 +8,7 @@ jest.mock('./api_keys'); jest.mock('./authenticator'); import Boom from 'boom'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { loggingSystemMock, diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index ab8e42a6a72da..68d3e622a6570 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { UnwrapPromise } from '@kbn/utility-types'; +import type { PublicMethodsOf, UnwrapPromise } from '@kbn/utility-types'; import { ILegacyClusterClient, KibanaRequest, diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index 7b2ad510db968..6b13c32a2f1ed 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -5,6 +5,7 @@ */ import { deepFreeze } from '@kbn/std'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { KibanaRequest, Logger, diff --git a/x-pack/plugins/security/server/authentication/providers/basic.test.ts b/x-pack/plugins/security/server/authentication/providers/basic.test.ts index 2481844abb389..87002ebed5672 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.test.ts @@ -101,13 +101,13 @@ describe('BasicAuthenticationProvider', () => { await expect( provider.authenticate( httpServerMock.createKibanaRequest({ - path: '/s/foo/some-path # that needs to be encoded', + path: '/s/foo/some path that needs to be encoded', }), null ) ).resolves.toEqual( AuthenticationResult.redirectTo( - '/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + '/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome%2520path%2520that%2520needs%2520to%2520be%2520encoded' ) ); }); diff --git a/x-pack/plugins/security/server/authentication/providers/basic.ts b/x-pack/plugins/security/server/authentication/providers/basic.ts index 35ab2d242659a..28b671346ee7f 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.ts @@ -90,7 +90,9 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -106,7 +108,9 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Redirecting request to Login page.'); const basePath = this.options.basePath.get(request); return AuthenticationResult.redirectTo( - `${basePath}/login?next=${encodeURIComponent(`${basePath}${request.url.path}`)}` + `${basePath}/login?next=${encodeURIComponent( + `${basePath}${request.url.pathname}${request.url.search}` + )}` ); } @@ -119,7 +123,7 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. diff --git a/x-pack/plugins/security/server/authentication/providers/http.ts b/x-pack/plugins/security/server/authentication/providers/http.ts index 3e33a52cbbc6b..933685d68978f 100644 --- a/x-pack/plugins/security/server/authentication/providers/http.ts +++ b/x-pack/plugins/security/server/authentication/providers/http.ts @@ -56,7 +56,9 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { * @param request Request instance. */ public async authenticate(request: KibanaRequest) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request); if (authorizationHeader == null) { @@ -72,12 +74,12 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { try { const user = await this.getUser(request); this.logger.debug( - `Request to ${request.url.path} has been authenticated via authorization header with "${authorizationHeader.scheme}" scheme.` + `Request to ${request.url.pathname}${request.url.search} has been authenticated via authorization header with "${authorizationHeader.scheme}" scheme.` ); return AuthenticationResult.succeeded(user); } catch (err) { this.logger.debug( - `Failed to authenticate request to ${request.url.path} via authorization header with "${authorizationHeader.scheme}" scheme: ${err.message}` + `Failed to authenticate request to ${request.url.pathname}${request.url.search} via authorization header with "${authorizationHeader.scheme}" scheme: ${err.message}` ); return AuthenticationResult.failed(err); } diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.ts index 5b593851cc2f2..d7de71f4da9ed 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.ts @@ -65,7 +65,9 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request); if (authorizationHeader && authorizationHeader.scheme.toLowerCase() !== 'negotiate') { @@ -100,7 +102,7 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.ts b/x-pack/plugins/security/server/authentication/providers/oidc.ts index 75c909cdcd94b..9570c59f8ea1d 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.ts @@ -166,7 +166,9 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -418,7 +420,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. @@ -477,7 +479,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { `${ this.options.basePath.serverBasePath }/internal/security/capture-url?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.path}` + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent( this.options.name )}`, diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index f3cc21500df26..6dcb448e08150 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -61,7 +61,9 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -105,7 +107,7 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index cf6772332b8b6..59a1782c1f1fd 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -193,7 +193,9 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -232,7 +234,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Normally when there is no active session in Kibana, `logout` method shouldn't do anything // and user will eventually be redirected to the home page to log in. But when SAML SLO is @@ -631,7 +633,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { `${ this.options.basePath.serverBasePath }/internal/security/capture-url?next=${encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.path}` + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` )}&providerType=${encodeURIComponent(this.type)}&providerName=${encodeURIComponent( this.options.name )}`, diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 0264edf4fc082..ffb1c89b24e47 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -173,13 +173,13 @@ describe('TokenAuthenticationProvider', () => { await expect( provider.authenticate( httpServerMock.createKibanaRequest({ - path: '/s/foo/some-path # that needs to be encoded', + path: '/s/foo/some path that needs to be encoded', }), null ) ).resolves.toEqual( AuthenticationResult.redirectTo( - '/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + '/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome%2520path%2520that%2520needs%2520to%2520be%2520encoded' ) ); }); diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 869fd69173e2e..7dace488bc95a 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -92,7 +92,9 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { * @param [state] Optional state object associated with the provider. */ public async authenticate(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + this.logger.debug( + `Trying to authenticate user request to ${request.url.pathname}${request.url.search}.` + ); if (HTTPAuthorizationHeader.parseFromRequest(request) != null) { this.logger.debug('Cannot authenticate requests with `Authorization` header.'); @@ -126,7 +128,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { * @param state State value previously stored by the provider. */ public async logout(request: KibanaRequest, state?: ProviderState | null) { - this.logger.debug(`Trying to log user out via ${request.url.path}.`); + this.logger.debug(`Trying to log user out via ${request.url.pathname}${request.url.search}.`); // Having a `null` state means that provider was specifically called to do a logout, but when // session isn't defined then provider is just being probed whether or not it can perform logout. @@ -241,7 +243,9 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { * @param request Request instance. */ private getLoginPageURL(request: KibanaRequest) { - const nextURL = encodeURIComponent(`${this.options.basePath.get(request)}${request.url.path}`); + const nextURL = encodeURIComponent( + `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}` + ); return `${this.options.basePath.get(request)}/login?next=${nextURL}`; } } diff --git a/x-pack/plugins/security/server/authorization/api_authorization.ts b/x-pack/plugins/security/server/authorization/api_authorization.ts index 813ed8d064d94..9cf090ab271ae 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.ts @@ -33,11 +33,13 @@ export function initAPIAuthorization( // we've actually authorized the request if (checkPrivilegesResponse.hasAllRequested) { - logger.debug(`User authorized for "${request.url.path}"`); + logger.debug(`User authorized for "${request.url.pathname}${request.url.search}"`); return toolkit.next(); } - logger.warn(`User not authorized for "${request.url.path}": responding with 403`); + logger.warn( + `User not authorized for "${request.url.pathname}${request.url.search}": responding with 403` + ); return response.forbidden(); }); } diff --git a/x-pack/plugins/security/server/authorization/authorization_service.tsx b/x-pack/plugins/security/server/authorization/authorization_service.tsx index 9547295af4dfb..a45bca90d8b56 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.tsx +++ b/x-pack/plugins/security/server/authorization/authorization_service.tsx @@ -168,7 +168,7 @@ export class AuthorizationService { http.registerOnPreResponse((request, preResponse, toolkit) => { if (preResponse.statusCode === 403 && canRedirectRequest(request)) { const basePath = http.basePath.get(request); - const next = `${basePath}${request.url.path}`; + const next = `${basePath}${request.url.pathname}${request.url.search}`; const regularBundlePath = `${basePath}/${buildNumber}/bundles`; const logoutUrl = http.basePath.prepend( diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index 5d8a7ae7bdfea..7eaa619b330e0 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -135,7 +135,7 @@ export function defineOIDCRoutes({ loginAttempt = { type: OIDCLogin.LoginWithAuthorizationCodeFlow, // We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway. - authenticationResponseURI: request.url.path!, + authenticationResponseURI: request.url.pathname + request.url.search, }; } else if (request.query.iss) { logger.warn( diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 7880e95240ff0..079c9e8ab9ce7 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { KibanaFeature } from '../../../features/server'; import { HttpResources, diff --git a/x-pack/plugins/security/server/routes/session_management/info.test.ts b/x-pack/plugins/security/server/routes/session_management/info.test.ts index fa9cba61df018..a104336f2e6ba 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.test.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.test.ts @@ -11,6 +11,7 @@ import { RequestHandlerContext, RouteConfig, } from '../../../../../../src/core/server'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { Session } from '../../session_management'; import { defineSessionInfoRoutes } from './info'; diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index 51bee1c74afa7..5e0f0a6005fb2 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -6,6 +6,7 @@ import { errors } from 'elasticsearch'; import { ObjectType } from '@kbn/config-schema'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ILegacyClusterClient, IRouter, diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts index 9b73358223b3d..b513230b3ba6f 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { RequestHandler, RouteConfig, diff --git a/x-pack/plugins/security/server/routes/views/logged_out.test.ts b/x-pack/plugins/security/server/routes/views/logged_out.test.ts index c160c4a26a187..31096bc33d686 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.test.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { HttpResourcesRequestHandler, RouteConfig } from '../../../../../../src/core/server'; import { Session } from '../../session_management'; import { defineLoggedOutRoutes } from './logged_out'; diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index fee3adbb19f97..b90a44be7aade 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -100,7 +100,7 @@ describe('Login view routes', () => { auth: { isAuthenticated: true }, }); (request as any).url = new URL( - `${request.url.path}${request.url.search}`, + `${request.url.pathname}${request.url.search}`, 'https://kibana.co' ); license.getFeatures.mockReturnValue({ showLogin: true } as any); @@ -114,7 +114,7 @@ describe('Login view routes', () => { // Redirect if `showLogin` is `false` even if user is not authenticated. request = httpServerMock.createKibanaRequest({ query, auth: { isAuthenticated: false } }); (request as any).url = new URL( - `${request.url.path}${request.url.search}`, + `${request.url.pathname}${request.url.search}`, 'https://kibana.co' ); license.getFeatures.mockReturnValue({ showLogin: false } as any); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index c7a3f31cc517e..85e8e21da81b0 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, diff --git a/x-pack/plugins/security/server/session_management/session.mock.ts b/x-pack/plugins/security/server/session_management/session.mock.ts index c09d24ba315c8..973341acbfce3 100644 --- a/x-pack/plugins/security/server/session_management/session.mock.ts +++ b/x-pack/plugins/security/server/session_management/session.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; import { Session, SessionValue } from './session'; import { sessionIndexMock } from './session_index.mock'; diff --git a/x-pack/plugins/security/server/session_management/session.test.ts b/x-pack/plugins/security/server/session_management/session.test.ts index c4d2342df36dc..3010e70c31421 100644 --- a/x-pack/plugins/security/server/session_management/session.test.ts +++ b/x-pack/plugins/security/server/session_management/session.test.ts @@ -6,6 +6,7 @@ import crypto from 'crypto'; import nodeCrypto from '@elastic/node-crypto'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ConfigSchema, createConfig } from '../config'; import { Session, SessionValueContentToEncrypt } from './session'; import { SessionIndex } from './session_index'; diff --git a/x-pack/plugins/security/server/session_management/session.ts b/x-pack/plugins/security/server/session_management/session.ts index 57c6509147665..a85369a6f4032 100644 --- a/x-pack/plugins/security/server/session_management/session.ts +++ b/x-pack/plugins/security/server/session_management/session.ts @@ -5,6 +5,7 @@ */ import nodeCrypto, { Crypto } from '@elastic/node-crypto'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { promisify } from 'util'; import { randomBytes, createHash } from 'crypto'; import { Duration } from 'moment'; diff --git a/x-pack/plugins/security/server/session_management/session_cookie.mock.ts b/x-pack/plugins/security/server/session_management/session_cookie.mock.ts index 026117f227561..998bc0ff01b23 100644 --- a/x-pack/plugins/security/server/session_management/session_cookie.mock.ts +++ b/x-pack/plugins/security/server/session_management/session_cookie.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SessionCookie, SessionCookieValue } from './session_cookie'; export const sessionCookieMock = { diff --git a/x-pack/plugins/security/server/session_management/session_index.mock.ts b/x-pack/plugins/security/server/session_management/session_index.mock.ts index 81dbe4b7410b8..ee8f5063655cc 100644 --- a/x-pack/plugins/security/server/session_management/session_index.mock.ts +++ b/x-pack/plugins/security/server/session_management/session_index.mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SessionIndex, SessionIndexValue } from './session_index'; export const sessionIndexMock = { diff --git a/x-pack/plugins/security/server/session_management/session_index.test.ts b/x-pack/plugins/security/server/session_management/session_index.test.ts index f4ff5a8bddb74..cba63412bf502 100644 --- a/x-pack/plugins/security/server/session_management/session_index.test.ts +++ b/x-pack/plugins/security/server/session_management/session_index.test.ts @@ -155,6 +155,17 @@ describe('Session index', () => { await sessionIndex.initialize(); }); + + it('works properly after failure', async () => { + const unexpectedError = new Error('Uh! Oh!'); + mockClusterClient.callAsInternalUser.mockImplementationOnce(() => + Promise.reject(unexpectedError) + ); + mockClusterClient.callAsInternalUser.mockImplementationOnce(() => Promise.resolve(true)); + + await expect(sessionIndex.initialize()).rejects.toBe(unexpectedError); + await expect(sessionIndex.initialize()).resolves.toBe(undefined); + }); }); describe('cleanUp', () => { diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 191e71f14d66d..ee503acc0d3a4 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -276,7 +276,7 @@ export class SessionIndex { } const sessionIndexTemplateName = `${this.options.kibanaIndexName}_security_session_index_template_${SESSION_INDEX_TEMPLATE_VERSION}`; - return (this.indexInitialization = new Promise(async (resolve) => { + return (this.indexInitialization = new Promise(async (resolve, reject) => { // Check if required index template exists. let indexTemplateExists = false; try { @@ -288,7 +288,7 @@ export class SessionIndex { this.options.logger.error( `Failed to check if session index template exists: ${err.message}` ); - throw err; + return reject(err); } // Create index template if it doesn't exist. @@ -303,7 +303,7 @@ export class SessionIndex { this.options.logger.debug('Successfully created session index template.'); } catch (err) { this.options.logger.error(`Failed to create session index template: ${err.message}`); - throw err; + return reject(err); } } @@ -316,7 +316,7 @@ export class SessionIndex { }); } catch (err) { this.options.logger.error(`Failed to check if session index exists: ${err.message}`); - throw err; + return reject(err); } // Create index if it doesn't exist. @@ -334,13 +334,14 @@ export class SessionIndex { this.options.logger.debug('Session index already exists.'); } else { this.options.logger.error(`Failed to create session index: ${err.message}`); - throw err; + return reject(err); } } } // Notify any consumers that are awaiting on this promise and immediately reset it. resolve(); + }).finally(() => { this.indexInitialization = undefined; })); } diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index 90483d7c0a4d5..cf3c787e6b0db 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -59,7 +59,7 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens type: 'boolean', }, authProviderCount: { - type: 'number', + type: 'long', }, enabledAuthProviders: { type: 'array', diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 882b3e5182bf3..79157018c315a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -860,6 +860,7 @@ type KbnConfigSchemaNonOptionalProps> = Pi */ export interface PolicyConfig { windows: { + advanced?: {}; events: { dll_and_driver_load: boolean; dns: boolean; @@ -881,6 +882,7 @@ export interface PolicyConfig { }; }; mac: { + advanced?: {}; events: { file: boolean; process: boolean; @@ -898,6 +900,7 @@ export interface PolicyConfig { }; }; linux: { + advanced?: {}; events: { file: boolean; process: boolean; @@ -916,15 +919,15 @@ export interface UIPolicyConfig { /** * Windows-specific policy configuration that is supported via the UI */ - windows: Pick; + windows: Pick; /** * Mac-specific policy configuration that is supported via the UI */ - mac: Pick; + mac: Pick; /** * Linux-specific policy configuration that is supported via the UI */ - linux: Pick; + linux: Pick; } /** Policy: Malware protection fields */ diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index 07d0d63e57059..db841d2a732c4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -30,8 +30,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/77957 -describe.skip('Alerts', () => { +describe('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index 7ccd588e16a89..542cf4ad8178f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -12,7 +12,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { OVERVIEW_URL } from '../urls/navigation'; import { esArchiverUnload, esArchiverLoad } from '../tasks/es_archiver'; -describe('Overview Page', () => { +describe.skip('Overview Page', () => { before(() => { cy.stubSearchStrategyApi('overviewHostQuery', 'overview_search_strategy'); cy.stubSearchStrategyApi('overviewNetworkQuery', 'overview_search_strategy'); @@ -35,7 +35,7 @@ describe('Overview Page', () => { }); }); - describe('with no data', () => { + describe.skip('with no data', () => { before(() => { esArchiverUnload('auditbeat'); loginAndWaitForPage(OVERVIEW_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts index c2ff2c58687f3..383ebe2220585 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -13,8 +13,7 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; import { waitsForEventsToBeLoaded, openEventsViewerFieldsBrowser } from '../tasks/hosts/events'; import { removeColumn, resetFields } from '../tasks/timeline'; -// FLAKY: https://github.com/elastic/kibana/issues/75794 -describe.skip('persistent timeline', () => { +describe('persistent timeline', () => { before(() => { loginAndWaitForPage(HOSTS_URL); openEvents(); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index f68ad88f578c7..0d0ea8460edf1 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -47,9 +47,9 @@ export const RULE_CHECKBOX = '.euiTableRow .euiCheckbox__input'; export const RULE_NAME = '[data-test-subj="ruleName"]'; -export const RULE_SWITCH = '[data-test-subj="rule-switch"]'; +export const RULE_SWITCH = '[data-test-subj="ruleSwitch"]'; -export const RULE_SWITCH_LOADER = '[data-test-subj="rule-switch-loader"]'; +export const RULE_SWITCH_LOADER = '[data-test-subj="ruleSwitchLoader"]'; export const RULES_TABLE = '[data-test-subj="rules-table"]'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/translations.ts b/x-pack/plugins/security_solution/public/cases/components/create/translations.ts index 222f8913b3fbd..38916dbddc7d7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/create/translations.ts @@ -18,6 +18,6 @@ export const STEP_ONE_TITLE = i18n.translate( export const STEP_TWO_TITLE = i18n.translate( 'xpack.securitySolution.components.create.stepTwoTitle', { - defaultMessage: 'External incident management system fields', + defaultMessage: 'External Connector Fields', } ); diff --git a/x-pack/plugins/security_solution/public/cases/pages/translations.ts b/x-pack/plugins/security_solution/public/cases/pages/translations.ts index 5b595c5892ef2..8ba4c4faf1876 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/pages/translations.ts @@ -224,7 +224,7 @@ export const GO_TO_DOCUMENTATION = i18n.translate( ); export const CONNECTORS = i18n.translate('xpack.securitySolution.case.caseView.connectors', { - defaultMessage: 'External incident management system', + defaultMessage: 'External Incident Management System', }); export const EDIT_CONNECTOR = i18n.translate('xpack.securitySolution.case.caseView.editConnector', { diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index 003439422442b..a0b5f71db7df0 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -224,7 +224,7 @@ export const GO_TO_DOCUMENTATION = i18n.translate( ); export const CONNECTORS = i18n.translate('xpack.securitySolution.case.caseView.connectors', { - defaultMessage: 'External incident management system', + defaultMessage: 'External Incident Management System', }); export const EDIT_CONNECTOR = i18n.translate('xpack.securitySolution.case.caseView.editConnector', { diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/translations.ts b/x-pack/plugins/security_solution/public/common/components/threat_match/translations.ts index ca9f6a13856cf..57e7416731486 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/translations.ts @@ -13,7 +13,7 @@ export const FIELD = i18n.translate('xpack.securitySolution.threatMatch.fieldDes export const THREAT_FIELD = i18n.translate( 'xpack.securitySolution.threatMatch.threatFieldDescription', { - defaultMessage: 'Threat index field', + defaultMessage: 'Indicator index field', } ); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx new file mode 100644 index 0000000000000..e81b52f281519 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndexField } from '../../../../common/search_strategy/index_fields'; +import { getBrowserFields } from '.'; +import { mockBrowserFields, mocksSource } from './mock'; + +describe('source/index.tsx', () => { + describe('getBrowserFields', () => { + test('it returns an empty object given an empty array', () => { + const fields = getBrowserFields('title 1', []); + expect(fields).toEqual({}); + }); + + test('it returns the same input with the same title', () => { + getBrowserFields('title 1', []); + // Since it is memoized it will return the same output which is empty object given 'title 1' a second time + const fields = getBrowserFields('title 1', mocksSource.indexFields as IndexField[]); + expect(fields).toEqual({}); + }); + + test('it transforms input into output as expected', () => { + const fields = getBrowserFields('title 2', mocksSource.indexFields as IndexField[]); + expect(fields).toEqual(mockBrowserFields); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index c36e2de61fcbf..47e550b2ced0f 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from '@elastic/safer-lodash-set/fp'; import { keyBy, pick, isEmpty, isEqual, isUndefined } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -55,15 +54,31 @@ export const getIndexFields = memoizeOne( (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length ); +/** + * HOT Code path where the fields can be 16087 in length or larger. This is + * VERY mutatious on purpose to improve the performance of the transform. + */ export const getBrowserFields = memoizeOne( - (_title: string, fields: IndexField[]): BrowserFields => - fields && fields.length > 0 - ? fields.reduce( - (accumulator: BrowserFields, field: IndexField) => - set([field.category, 'fields', field.name], field, accumulator), - {} - ) - : {}, + (_title: string, fields: IndexField[]): BrowserFields => { + // Adds two dangerous casts to allow for mutations within this function + type DangerCastForMutation = Record; + type DangerCastForBrowserFieldsMutation = Record< + string, + Omit & { fields: Record } + >; + + // We mutate this instead of using lodash/set to keep this as fast as possible + return fields.reduce((accumulator, field) => { + if (accumulator[field.category] == null) { + (accumulator as DangerCastForMutation)[field.category] = {}; + } + if (accumulator[field.category].fields == null) { + accumulator[field.category].fields = {}; + } + accumulator[field.category].fields[field.name] = (field as unknown) as BrowserField; + return accumulator; + }, {}); + }, // Update the value only if _title has changed (newArgs, lastArgs) => newArgs[0] === lastArgs[0] ); @@ -194,15 +209,14 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { const { data, notifications } = useKibana().services; const abortCtrl = useRef(new AbortController()); const dispatch = useDispatch(); - const previousIndexesName = useRef([]); - const indexNamesSelectedSelector = useMemo( () => sourcererSelectors.getIndexNamesSelectedSelector(), [] ); - const indexNames = useShallowEqualSelector((state) => - indexNamesSelectedSelector(state, sourcererScopeName) - ); + const { indexNames, previousIndexNames } = useShallowEqualSelector<{ + indexNames: string[]; + previousIndexNames: string; + }>((state) => indexNamesSelectedSelector(state, sourcererScopeName)); const setLoading = useCallback( (loading: boolean) => { @@ -230,7 +244,6 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { if (!response.isPartial && !response.isRunning) { if (!didCancel) { const stringifyIndices = response.indicesExist.sort().join(); - previousIndexesName.current = response.indicesExist; dispatch( sourcererActions.setSource({ id: sourcererScopeName, @@ -279,8 +292,8 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { ); useEffect(() => { - if (!isEmpty(indexNames) && !isEqual(previousIndexesName.current, indexNames)) { + if (!isEmpty(indexNames) && previousIndexNames !== indexNames.sort().join()) { indexFieldsSearch(indexNames); } - }, [indexNames, indexFieldsSearch, previousIndexesName]); + }, [indexNames, indexFieldsSearch, previousIndexNames]); }; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index accfb38bc3dc1..22cb4b91fd839 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -86,7 +86,29 @@ jest.mock('../../utils/apollo_context', () => ({ })); describe('Sourcerer Hooks', () => { - const state: State = mockGlobalState; + const state: State = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + indexPattern: { + fields: [], + title: '', + }, + }, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + indexPattern: { + fields: [], + title: '', + }, + }, + }, + }, + }; const { storage } = createSecuritySolutionStorageMock(); let store = createStore( state, diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index b02a09625ccf3..d9f2abeb3832e 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -16,6 +16,9 @@ import { ManageScope, SourcererScopeName } from '../../store/sourcerer/model'; import { useIndexFields } from '../source'; import { State } from '../../store'; import { useUserInfo } from '../../../detections/components/user_info'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { TimelineId } from '../../../../common/types/timeline'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; export const useInitSourcerer = ( scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default @@ -29,6 +32,12 @@ export const useInitSourcerer = ( ); const ConfigIndexPatterns = useSelector(getConfigIndexPatternsSelector, isEqual); + const getTimelineSelector = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const activeTimeline = useSelector( + (state) => getTimelineSelector(state, TimelineId.active), + isEqual + ); + useIndexFields(scopeId); useIndexFields(SourcererScopeName.timeline); @@ -40,7 +49,11 @@ export const useInitSourcerer = ( // Related to timeline useEffect(() => { - if (!loadingSignalIndex && signalIndexName != null) { + if ( + !loadingSignalIndex && + signalIndexName != null && + (activeTimeline == null || (activeTimeline != null && activeTimeline.savedObjectId == null)) + ) { dispatch( sourcererActions.setSelectedIndexPatterns({ id: SourcererScopeName.timeline, @@ -48,7 +61,7 @@ export const useInitSourcerer = ( }) ); } - }, [ConfigIndexPatterns, dispatch, loadingSignalIndex, signalIndexName]); + }, [activeTimeline, ConfigIndexPatterns, dispatch, loadingSignalIndex, signalIndexName]); // Related to the detection page useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts index 0b40586798f09..8e92d7559f1d6 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts @@ -34,3 +34,9 @@ export const setSelectedIndexPatterns = actionCreator<{ selectedPatterns: string[]; eventType?: TimelineEventsType; }>('SET_SELECTED_INDEX_PATTERNS'); + +export const initTimelineIndexPatterns = actionCreator<{ + id: SourcererScopeName; + selectedPatterns: string[]; + eventType?: TimelineEventsType; +}>('INIT_TIMELINE_INDEX_PATTERNS'); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts index 3ae9740cfd51d..42a4fe73c43ba 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts @@ -25,16 +25,7 @@ export const createDefaultIndexPatterns = ({ eventType, id, selectedPatterns, st if (isEmpty(newSelectedPatterns)) { let defaultIndexPatterns = state.configIndexPatterns; if (id === SourcererScopeName.timeline && isEmpty(newSelectedPatterns)) { - if (eventType === 'all' && !isEmpty(state.signalIndexName)) { - defaultIndexPatterns = [...state.configIndexPatterns, state.signalIndexName ?? '']; - } else if (eventType === 'raw') { - defaultIndexPatterns = state.configIndexPatterns; - } else if ( - !isEmpty(state.signalIndexName) && - (eventType === 'signal' || eventType === 'alert') - ) { - defaultIndexPatterns = [state.signalIndexName ?? '']; - } + defaultIndexPatterns = defaultIndexPatternByEventType({ state, eventType }); } else if (id === SourcererScopeName.detections && isEmpty(newSelectedPatterns)) { defaultIndexPatterns = [state.signalIndexName ?? '']; } @@ -42,3 +33,21 @@ export const createDefaultIndexPatterns = ({ eventType, id, selectedPatterns, st } return newSelectedPatterns; }; + +export const defaultIndexPatternByEventType = ({ + state, + eventType, +}: { + state: SourcererModel; + eventType?: TimelineEventsType; +}) => { + let defaultIndexPatterns = state.configIndexPatterns; + if (eventType === 'all' && !isEmpty(state.signalIndexName)) { + defaultIndexPatterns = [...state.configIndexPatterns, state.signalIndexName ?? '']; + } else if (eventType === 'raw') { + defaultIndexPatterns = state.configIndexPatterns; + } else if (!isEmpty(state.signalIndexName) && (eventType === 'signal' || eventType === 'alert')) { + defaultIndexPatterns = [state.signalIndexName ?? '']; + } + return defaultIndexPatterns; +}; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts index a1112607de24f..0c7c52e5e5733 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// Prefer importing entire lodash library, e.g. import { get } from "lodash" - +import { isEmpty } from 'lodash/fp'; import { reducerWithInitialState } from 'typescript-fsa-reducers'; import { @@ -14,9 +13,10 @@ import { setSelectedIndexPatterns, setSignalIndexName, setSource, + initTimelineIndexPatterns, } from './actions'; import { initialSourcererState, SourcererModel } from './model'; -import { createDefaultIndexPatterns } from './helpers'; +import { createDefaultIndexPatterns, defaultIndexPatternByEventType } from './helpers'; export type SourcererState = SourcererModel; @@ -52,6 +52,21 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState) }, }; }) + .case(initTimelineIndexPatterns, (state, { id, selectedPatterns, eventType }) => { + return { + ...state, + sourcererScopes: { + ...state.sourcererScopes, + [id]: { + ...state.sourcererScopes[id], + selectedPatterns: isEmpty(selectedPatterns) + ? defaultIndexPatternByEventType({ state, eventType }) + : selectedPatterns, + }, + }, + }; + }) + .case(setSource, (state, { id, payload }) => { const { ...sourcererScopes } = payload; return { diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts index f0859a0fb9b66..e7bd6234cb207 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts @@ -41,13 +41,18 @@ export const getIndexNamesSelectedSelector = () => { const getScopesSelector = scopesSelector(); const getConfigIndexPatternsSelector = configIndexPatternsSelector(); - const mapStateToProps = (state: State, scopeId: SourcererScopeName): string[] => { + const mapStateToProps = ( + state: State, + scopeId: SourcererScopeName + ): { indexNames: string[]; previousIndexNames: string } => { const scope = getScopesSelector(state)[scopeId]; const configIndexPatterns = getConfigIndexPatternsSelector(state); - - return scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns; + return { + indexNames: + scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns, + previousIndexNames: scope.indexPattern.title, + }; }; - return mapStateToProps; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index bfc104b105236..ecc0fc54d0d47 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -47,7 +47,9 @@ describe('alert actions', () => { searchStrategyClient = { aggs: {} as ISearchStart['aggs'], showError: jest.fn(), - search: jest.fn().mockResolvedValue({ data: mockTimelineDetails }), + search: jest + .fn() + .mockImplementation(() => ({ toPromise: () => ({ data: mockTimelineDetails }) })), searchSource: {} as ISearchStart['searchSource'], session: dataPluginMock.createStartContract().search.session, }; @@ -400,6 +402,78 @@ describe('alert actions', () => { expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); }); }); + + describe('Eql', () => { + test(' with signal.group.id', async () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithAlert, + signal: { + rule: { + ...mockEcsDataWithAlert.signal?.rule!, + type: ['eql'], + timeline_id: [''], + }, + group: { + id: ['my-group-id'], + }, + }, + }; + + await sendAlertToTimelineAction({ + createTimeline, + ecsData: ecsDataMock, + nonEcsData: [], + updateTimelineIsLoading, + searchStrategyClient, + }); + + expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith({ + ...defaultTimelineProps, + timeline: { + ...defaultTimelineProps.timeline, + dataProviders: [ + { + and: [], + enabled: true, + excluded: false, + id: + 'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-alert-id-my-group-id', + kqlQuery: '', + name: '1', + queryMatch: { field: 'signal.group.id', operator: ':', value: 'my-group-id' }, + }, + ], + }, + }); + }); + + test(' with NO signal.group.id', async () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithAlert, + signal: { + rule: { + ...mockEcsDataWithAlert.signal?.rule!, + type: ['eql'], + timeline_id: [''], + }, + }, + }; + + await sendAlertToTimelineAction({ + createTimeline, + ecsData: ecsDataMock, + nonEcsData: [], + updateTimelineIsLoading, + searchStrategyClient, + }); + + expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); }); describe('determineToAndFrom', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 043a5afc4480d..e3defaea2ec67 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -150,8 +150,10 @@ export const getThresholdAggregationDataProvider = ( ]; }; -export const isEqlRule = (ecsData: Ecs) => - ecsData.signal?.rule?.type?.length && ecsData.signal?.rule?.type[0] === 'eql'; +export const isEqlRuleWithGroupId = (ecsData: Ecs) => + ecsData.signal?.rule?.type?.length && + ecsData.signal?.rule?.type[0] === 'eql' && + ecsData.signal?.group?.id?.length; export const isThresholdRule = (ecsData: Ecs) => ecsData.signal?.rule?.type?.length && ecsData.signal?.rule?.type[0] === 'threshold'; @@ -181,24 +183,23 @@ export const sendAlertToTimelineAction = async ({ timelineType: TimelineType.template, }, }), - searchStrategyClient.search< - TimelineEventsDetailsRequestOptions, - TimelineEventsDetailsStrategyResponse - >( - { - defaultIndex: [], - docValueFields: [], - indexName: ecsData._index ?? '', - eventId: ecsData._id, - factoryQueryType: TimelineEventsQueries.details, - }, - { - strategy: 'securitySolutionTimelineSearchStrategy', - } - ), + searchStrategyClient + .search( + { + defaultIndex: [], + docValueFields: [], + indexName: ecsData._index ?? '', + eventId: ecsData._id, + factoryQueryType: TimelineEventsQueries.details, + }, + { + strategy: 'securitySolutionTimelineSearchStrategy', + } + ) + .toPromise(), ]); const resultingTimeline: TimelineResult = getOr({}, 'data.getOneTimeline', responseTimeline); - const eventData: TimelineEventsDetailsItem[] = getOr([], 'data', eventDataResp); + const eventData: TimelineEventsDetailsItem[] = eventDataResp.data ?? []; if (!isEmpty(resultingTimeline)) { const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline); const { timeline, notes } = formatTimelineResultToModel( @@ -327,7 +328,7 @@ export const sendAlertToTimelineAction = async ({ }, }, ]; - if (isEqlRule(ecsData)) { + if (isEqlRuleWithGroupId(ecsData)) { const signalGroupId = ecsData.signal?.group?.id?.length ? ecsData.signal?.group?.id[0] : 'unknown-signal-group-id'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index ebdfdcc262b34..ee1edecbdc54a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -437,7 +437,7 @@ describe('helpers', () => { it('returns a humanized description for a threat_match type', () => { const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'threat_match'); - expect(result.description).toEqual('Threat Match'); + expect(result.description).toEqual('Indicator Match'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx index d9186c2da7225..04647871f212e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx @@ -65,7 +65,7 @@ export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate( export const THREAT_MATCH_TYPE_DESCRIPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.threatMatchRuleTypeDescription', { - defaultMessage: 'Threat Match', + defaultMessage: 'Indicator Match', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 604f86866d565..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RuleSwitch renders correctly against snapshot 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx index 104eff34c91b3..910a28927fd93 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx @@ -4,16 +4,173 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { waitFor } from '@testing-library/react'; +import { enableRules } from '../../../containers/detection_engine/rules'; +import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions'; import { RuleSwitchComponent } from './index'; +import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; +import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters'; + +jest.mock('../../../../common/components/toasters'); +jest.mock('../../../containers/detection_engine/rules'); +jest.mock('../../../pages/detection_engine/rules/all/actions'); describe('RuleSwitch', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - + beforeEach(() => { + (useStateToaster as jest.Mock).mockImplementation(() => [[], jest.fn()]); + (enableRules as jest.Mock).mockResolvedValue([getRulesSchemaMock()]); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders loader if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="ruleSwitchLoader"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').exists()).toBeFalsy(); + }); + + test('it renders switch disabled if "isDisabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().disabled).toBeTruthy(); + }); + + test('it renders switch enabled if "enabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeTruthy(); + }); + + test('it renders switch disabled if "enabled" is false', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeFalsy(); + }); + + test('it renders an off switch enabled on click', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(1).props().checked).toBeTruthy(); + }); + }); + + test('it renders an on switch off on click', async () => { + const rule: RulesSchema = { ...getRulesSchemaMock(), enabled: false }; + + (enableRules as jest.Mock).mockResolvedValue([rule]); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(1).props().checked).toBeFalsy(); + }); + }); + + test('it dispatches error toaster if "enableRules" call rejects', async () => { + const mockError = new Error('uh oh'); + (enableRules as jest.Mock).mockRejectedValue(mockError); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + ); - expect(wrapper).toMatchSnapshot(); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(displayErrorToast).toHaveBeenCalledTimes(1); + }); + }); + + test('it dispatches error toaster if "enableRules" call resolves with some errors', async () => { + (enableRules as jest.Mock).mockResolvedValue([ + getRulesSchemaMock(), + { error: { status_code: 400, message: 'error' } }, + { error: { status_code: 400, message: 'error' } }, + ]); + + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(displayErrorToast).toHaveBeenCalledTimes(1); + }); + }); + + test('it invokes "enableRulesAction" if dispatch is passed through', async () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(enableRulesAction).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx index 73d66bf024a62..1a9bcca7eb601 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.tsx @@ -13,7 +13,7 @@ import { } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import styled from 'styled-components'; -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useMemo, useCallback, useState, useEffect } from 'react'; import * as i18n from '../../../pages/detection_engine/rules/translations'; import { enableRules } from '../../../containers/detection_engine/rules'; @@ -63,8 +63,11 @@ export const RuleSwitchComponent = ({ if (dispatch != null) { await enableRulesAction([id], event.target.checked!, dispatch, dispatchToaster); } else { + const enabling = event.target.checked!; + const title = enabling + ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(1) + : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(1); try { - const enabling = event.target.checked!; const response = await enableRules({ ids: [id], enabled: enabling, @@ -73,9 +76,7 @@ export const RuleSwitchComponent = ({ if (errors.length > 0) { setMyIsLoading(false); - const title = enabling - ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(1) - : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(1); + displayErrorToast( title, errors.map((e) => e.error.message), @@ -88,8 +89,9 @@ export const RuleSwitchComponent = ({ onChange(rule.enabled); } } - } catch { + } catch (err) { setMyIsLoading(false); + displayErrorToast(title, err.message, dispatchToaster); } } setMyIsLoading(false); @@ -105,21 +107,22 @@ export const RuleSwitchComponent = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled]); - useEffect(() => { + const showLoader = useMemo((): boolean => { if (myIsLoading !== isLoading) { - setMyIsLoading(isLoading ?? false); + return isLoading ?? false; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading]); + + return myIsLoading; + }, [myIsLoading, isLoading]); return ( - {myIsLoading ? ( - + {showLoader ? ( + ) : ( = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatIndexPatternsLabel', { - defaultMessage: 'Threat index patterns', + defaultMessage: 'Indicator Index Patterns', } ), helpText: {THREAT_MATCH_INDEX_HELPER_TEXT}, @@ -265,7 +265,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatMappingLabel', { - defaultMessage: 'Threat Mapping', + defaultMessage: 'Indicator Mapping', } ), validations: [ @@ -301,7 +301,7 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThreatQueryBarLabel', { - defaultMessage: 'Threat index query', + defaultMessage: 'Indicator Index Query', } ), validations: [ diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 8076733be2d7d..0b708133d947b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -411,7 +411,12 @@ describe('Detections Rules API', () => { describe('createPrepackagedRules', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue('unknown'); + fetchMock.mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); }); test('check parameter url when creating pre-packaged rules', async () => { @@ -423,7 +428,12 @@ describe('Detections Rules API', () => { }); test('happy path', async () => { const resp = await createPrepackagedRules({ signal: abortCtrl.signal }); - expect(resp).toEqual(true); + expect(resp).toEqual({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 23adfe0228333..ce1fdd18dbdef 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -245,13 +245,25 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => { - await KibanaServices.get().http.fetch(DETECTION_ENGINE_PREPACKAGED_URL, { +export const createPrepackagedRules = async ({ + signal, +}: BasicFetchProps): Promise<{ + rules_installed: number; + rules_updated: number; + timelines_installed: number; + timelines_updated: number; +}> => { + const result = await KibanaServices.get().http.fetch<{ + rules_installed: number; + rules_updated: number; + timelines_installed: number; + timelines_updated: number; + }>(DETECTION_ENGINE_PREPACKAGED_URL, { method: 'PUT', signal, }); - return true; + return result; }; /** diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts index 721790a36b27f..6e2aee9658658 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts @@ -30,7 +30,21 @@ export const RULE_AND_TIMELINE_PREPACKAGED_FAILURE = i18n.translate( export const RULE_AND_TIMELINE_PREPACKAGED_SUCCESS = i18n.translate( 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleAndTimelineSuccesDescription', { - defaultMessage: 'Installed pre-packaged rules and timelines from elastic', + defaultMessage: 'Installed pre-packaged rules and timeline templates from elastic', + } +); + +export const RULE_PREPACKAGED_SUCCESS = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription', + { + defaultMessage: 'Installed pre-packaged rules from elastic', + } +); + +export const TIMELINE_PREPACKAGED_SUCCESS = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.createPrePackagedTimelineSuccesDescription', + { + defaultMessage: 'Installed pre-packaged timeline templates from elastic', } ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 7f74e92584494..f6bd8c4359d6e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -3,12 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { ReactElement } from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { ReturnPrePackagedRulesAndTimelines, usePrePackagedRules } from './use_pre_packaged_rules'; import * as api from './api'; +import { shallow } from 'enzyme'; +import * as i18n from './translations'; -jest.mock('./api'); +jest.mock('./api', () => ({ + getPrePackagedRulesStatus: jest.fn(), + createPrepackagedRules: jest.fn(), +})); describe('usePrePackagedRules', () => { beforeEach(() => { @@ -52,6 +57,21 @@ describe('usePrePackagedRules', () => { }); test('fetch getPrePackagedRulesStatus', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 33, + rules_installed: 12, + rules_not_installed: 0, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -87,7 +107,6 @@ describe('usePrePackagedRules', () => { }); test('happy path to createPrePackagedRules', async () => { - const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules'); await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -106,7 +125,7 @@ describe('usePrePackagedRules', () => { resp = await result.current.createPrePackagedRules(); } expect(resp).toEqual(true); - expect(spyOnCreatePrepackagedRules).toHaveBeenCalled(); + expect(api.createPrepackagedRules).toHaveBeenCalled(); expect(result.current).toEqual({ getLoadPrebuiltRulesAndTemplatesButton: result.current.getLoadPrebuiltRulesAndTemplatesButton, @@ -127,6 +146,253 @@ describe('usePrePackagedRules', () => { }); }); + test('getLoadPrebuiltRulesAndTemplatesButton - LOAD_PREPACKAGED_RULES', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 0, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + 'data-test-subj': 'button', + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="button"]').text()).toEqual(i18n.LOAD_PREPACKAGED_RULES); + }); + }); + + test('getLoadPrebuiltRulesAndTemplatesButton - LOAD_PREPACKAGED_TIMELINE_TEMPLATES', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 0, + rules_not_installed: 0, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 1, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + 'data-test-subj': 'button', + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="button"]').text()).toEqual( + i18n.LOAD_PREPACKAGED_TIMELINE_TEMPLATES + ); + }); + }); + + test('getLoadPrebuiltRulesAndTemplatesButton - LOAD_PREPACKAGED_RULES_AND_TEMPLATES', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 0, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 1, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + 'data-test-subj': 'button', + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="button"]').text()).toEqual( + i18n.LOAD_PREPACKAGED_RULES_AND_TEMPLATES + ); + }); + }); + + test('getReloadPrebuiltRulesAndTemplatesButton - missing rules and templates', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 1, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 1, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getReloadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="reloadPrebuiltRulesBtn"]').text()).toEqual( + 'Install 1 Elastic prebuilt rule and 1 Elastic prebuilt timeline ' + ); + }); + }); + + test('getReloadPrebuiltRulesAndTemplatesButton - missing rules', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 1, + rules_not_installed: 1, + rules_not_updated: 0, + timelines_installed: 0, + timelines_not_installed: 0, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getReloadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="reloadPrebuiltRulesBtn"]').text()).toEqual( + 'Install 1 Elastic prebuilt rule ' + ); + }); + }); + + test('getReloadPrebuiltRulesAndTemplatesButton - missing templates', async () => { + (api.getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_custom_installed: 0, + rules_installed: 1, + rules_not_installed: 0, + rules_not_updated: 0, + timelines_installed: 1, + timelines_not_installed: 1, + timelines_not_updated: 0, + }); + (api.createPrepackagedRules as jest.Mock).mockResolvedValue({ + rules_installed: 0, + rules_updated: 0, + timelines_installed: 0, + timelines_updated: 0, + }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePrePackagedRules({ + canUserCRUD: true, + hasIndexWrite: true, + isAuthenticated: true, + hasEncryptionKey: true, + isSignalIndexExists: true, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const button = result.current.getReloadPrebuiltRulesAndTemplatesButton({ + isDisabled: false, + onClick: jest.fn(), + }); + const wrapper = shallow(button as ReactElement); + expect(wrapper.find('[data-test-subj="reloadPrebuiltRulesBtn"]').text()).toEqual( + 'Install 1 Elastic prebuilt timeline ' + ); + }); + }); + test('unhappy path to createPrePackagedRules', async () => { const spyOnCreatePrepackagedRules = jest.spyOn(api, 'createPrepackagedRules'); spyOnCreatePrepackagedRules.mockImplementation(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 4d19f44bcfc84..48530ddeb181e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -114,6 +114,27 @@ export const usePrePackagedRules = ({ const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); + const getSuccessToastMessage = (result: { + rules_installed: number; + rules_updated: number; + timelines_installed: number; + timelines_updated: number; + }) => { + const { + rules_installed: rulesInstalled, + rules_updated: rulesUpdated, + timelines_installed: timelinesInstalled, + timelines_updated: timelinesUpdated, + } = result; + if (rulesInstalled === 0 && (timelinesInstalled > 0 || timelinesUpdated > 0)) { + return i18n.TIMELINE_PREPACKAGED_SUCCESS; + } else if ((rulesInstalled > 0 || rulesUpdated > 0) && timelinesInstalled === 0) { + return i18n.RULE_PREPACKAGED_SUCCESS; + } else { + return i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS; + } + }; + useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); @@ -170,7 +191,7 @@ export const usePrePackagedRules = ({ isSignalIndexExists ) { setLoadingCreatePrePackagedRules(true); - await createPrepackagedRules({ + const result = await createPrepackagedRules({ signal: abortCtrl.signal, }); @@ -209,11 +230,7 @@ export const usePrePackagedRules = ({ timelinesNotInstalled: prePackagedRuleStatusResponse.timelines_not_installed, timelinesNotUpdated: prePackagedRuleStatusResponse.timelines_not_updated, }); - - displaySuccessToast( - i18n.RULE_AND_TIMELINE_PREPACKAGED_SUCCESS, - dispatchToaster - ); + displaySuccessToast(getSuccessToastMessage(result), dispatchToaster); stopTimeOut(); resolve(true); } else { @@ -277,8 +294,9 @@ export const usePrePackagedRules = ({ ); const getLoadPrebuiltRulesAndTemplatesButton = useCallback( ({ isDisabled, onClick, fill, 'data-test-subj': dataTestSubj = 'loadPrebuiltRulesBtn' }) => { - return prePackagedRuleStatus === 'ruleNotInstalled' || - prePackagedTimelineStatus === 'timelinesNotInstalled' ? ( + return (prePackagedRuleStatus === 'ruleNotInstalled' || + prePackagedTimelineStatus === 'timelinesNotInstalled') && + prePackagedRuleStatus !== 'someRuleUninstall' ? ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts new file mode 100644 index 0000000000000..d25588dabedc6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -0,0 +1,315 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface AdvancedPolicySchemaType { + key: string; + first_supported_version: string; + last_supported_version?: string; + documentation: string; +} + +export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ + { + key: 'linux.advanced.agent.connection_delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.manifest_relative_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.global.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.user.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.user.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.user.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.artifacts.user.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.elasticsearch.delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'linux.advanced.elasticsearch.tls.verify_peer', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'linux.advanced.elasticsearch.tls.verify_hostname', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'linux.advanced.elasticsearch.tls.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.agent.connection_delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.manifest_relative_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.global.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.user.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.user.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.user.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.artifacts.user.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.elasticsearch.delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.elasticsearch.tls.verify_peer', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'mac.advanced.elasticsearch.tls.verify_hostname', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'mac.advanced.elasticsearch.tls.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.malware.quarantine', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.connect', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.harden', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.process', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.filewrite', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'mac.advanced.kernel.network', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.agent.connection_delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.manifest_relative_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.global.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.user.base_url', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.user.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.user.public_key', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.artifacts.user.interval', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.elasticsearch.delay', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.elasticsearch.tls.verify_peer', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'windows.advanced.elasticsearch.tls.verify_hostname', + first_supported_version: '7.11', + documentation: 'default is true', + }, + { + key: 'windows.advanced.elasticsearch.tls.ca_cert', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.malware.quarantine', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.ransomware.mbr', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.ransomware.canary', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.connect', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.harden', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.process', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.filewrite', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.network', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.fileopen', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.asyncimageload', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.syncimageload', + first_supported_version: '7.11', + documentation: '', + }, + { + key: 'windows.advanced.kernel.registry', + first_supported_version: '7.11', + documentation: '', + }, +]; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts index 4d32a9fbec694..8d5c19a9e489a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts @@ -4,46 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { cloneDeep } from 'lodash'; import { UIPolicyConfig } from '../../../../../common/endpoint/types'; -/** - * A typed Object.entries() function where the keys and values are typed based on the given object - */ -const entries = (o: T): Array<[keyof T, T[keyof T]]> => - Object.entries(o) as Array<[keyof T, T[keyof T]]>; -type DeepPartial = { [K in keyof T]?: DeepPartial }; - -/** - * Returns a deep copy of `UIPolicyConfig` object - */ -export function clone(policyDetailsConfig: UIPolicyConfig): UIPolicyConfig { - const clonedConfig: DeepPartial = {}; - for (const [key, val] of entries(policyDetailsConfig)) { - if (typeof val === 'object') { - const valClone: Partial = {}; - clonedConfig[key] = valClone; - for (const [key2, val2] of entries(val)) { - if (typeof val2 === 'object') { - valClone[key2] = { - ...val2, - }; - } else { - clonedConfig[key] = { - ...val, - }; - } - } - } else { - clonedConfig[key] = val; - } - } - - /** - * clonedConfig is typed as DeepPartial so we can construct the copy from an empty object - */ - return clonedConfig as UIPolicyConfig; -} - /** * Returns value from `configuration` */ @@ -69,7 +32,7 @@ export const setIn = (a: UIPolicyConfig) => (k >( v: V ): UIPolicyConfig => { - const c = clone(a); + const c = cloneDeep(a); c[key][subKey][leafKey] = v; return c; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index b76e0c8acf4c3..89ba05547f447 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -8,7 +8,6 @@ import { PolicyDetailsState } from '../../types'; import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; import { policyDetailsReducer, PolicyDetailsAction, policyDetailsMiddlewareFactory } from './index'; import { policyConfig } from './selectors'; -import { clone } from '../../models/policy_details_config'; import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; import { PolicyData } from '../../../../../../common/endpoint/types'; import { @@ -20,6 +19,7 @@ import { createAppRootMockRenderer, } from '../../../../../common/mock/endpoint'; import { HttpFetchOptions } from 'kibana/public'; +import { cloneDeep } from 'lodash'; describe('policy details: ', () => { let store: Store; @@ -93,7 +93,7 @@ describe('policy details: ', () => { throw new Error(); } - const newPayload1 = clone(config); + const newPayload1 = cloneDeep(config); newPayload1.windows.events.process = true; dispatch({ @@ -115,7 +115,7 @@ describe('policy details: ', () => { throw new Error(); } - const newPayload1 = clone(config); + const newPayload1 = cloneDeep(config); newPayload1.mac.events.file = true; dispatch({ @@ -137,7 +137,7 @@ describe('policy details: ', () => { throw new Error(); } - const newPayload1 = clone(config); + const newPayload1 = cloneDeep(config); newPayload1.linux.events.file = true; dispatch({ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts index 953438526b87e..f275124a73527 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts @@ -103,16 +103,19 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel (windows, mac, linux) => { return { windows: { + advanced: windows.advanced, events: windows.events, malware: windows.malware, popup: windows.popup, }, mac: { + advanced: mac.advanced, events: mac.events, malware: mac.malware, popup: mac.popup, }, linux: { + advanced: linux.advanced, events: linux.events, }, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts index 9c227ca81a426..ce942205b1620 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts @@ -6,3 +6,4 @@ export * from './policy_list'; export * from './policy_details'; +export * from './policy_advanced'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx new file mode 100644 index 0000000000000..b4b82b7f692b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { EuiFieldText, EuiFormRow, EuiPanel, EuiText } from '@elastic/eui'; +import { cloneDeep } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { policyConfig } from '../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from './policy_hooks'; +import { AdvancedPolicySchema } from '../models/advanced_policy_schema'; + +function setValue(obj: Record, value: string, path: string[]) { + let newPolicyConfig = obj; + for (let i = 0; i < path.length - 1; i++) { + if (!newPolicyConfig[path[i]]) { + newPolicyConfig[path[i]] = {} as Record; + } + newPolicyConfig = newPolicyConfig[path[i]] as Record; + } + newPolicyConfig[path[path.length - 1]] = value; +} + +function getValue(obj: Record, path: string[]) { + let currentPolicyConfig = obj; + + for (let i = 0; i < path.length - 1; i++) { + if (currentPolicyConfig[path[i]]) { + currentPolicyConfig = currentPolicyConfig[path[i]] as Record; + } else { + return undefined; + } + } + return currentPolicyConfig[path[path.length - 1]]; +} + +export const AdvancedPolicyForms = React.memo(() => { + return ( + <> + +

    + +

    +
    + + {AdvancedPolicySchema.map((advancedField, index) => { + const configPath = advancedField.key.split('.'); + return ( + + ); + })} + + + ); +}); + +AdvancedPolicyForms.displayName = 'AdvancedPolicyForms'; + +const PolicyAdvanced = React.memo( + ({ + configPath, + firstSupportedVersion, + lastSupportedVersion, + }: { + configPath: string[]; + firstSupportedVersion: string; + lastSupportedVersion?: string; + }) => { + const dispatch = useDispatch(); + const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); + const onChange = useCallback( + (event) => { + if (policyDetailsConfig) { + const newPayload = cloneDeep(policyDetailsConfig); + setValue( + (newPayload as unknown) as Record, + event.target.value, + configPath + ); + dispatch({ + type: 'userChangedPolicyConfig', + payload: { policyConfig: newPayload }, + }); + } + }, + [dispatch, policyDetailsConfig, configPath] + ); + + const value = + policyDetailsConfig && + getValue((policyDetailsConfig as unknown) as Record, configPath); + + return ( + <> + + {lastSupportedVersion + ? `${firstSupportedVersion}-${lastSupportedVersion}` + : `${firstSupportedVersion}+`} + + } + > + + + + ); + } +); + +PolicyAdvanced.displayName = 'PolicyAdvanced'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 40c982cfc071b..8fc5de48f36db 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -47,6 +47,7 @@ import { MANAGEMENT_APP_ID } from '../../../common/constants'; import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; import { WrapperPage } from '../../../../common/components/wrapper_page'; import { HeaderPage } from '../../../../common/components/header_page'; +import { AdvancedPolicyForms } from './policy_advanced'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); @@ -69,6 +70,7 @@ export const PolicyDetails = React.memo(() => { // Local state const [showConfirm, setShowConfirm] = useState(false); const [routeState, setRouteState] = useState(); + const [showAdvancedPolicy, setShowAdvancedPolicy] = useState(false); const policyName = policyItem?.name ?? ''; const hostListRouterPath = getEndpointListPath({ name: 'endpointList' }); @@ -128,6 +130,10 @@ export const PolicyDetails = React.memo(() => { setShowConfirm(false); }, []); + const handleAdvancedPolicyClick = useCallback(() => { + setShowAdvancedPolicy(!showAdvancedPolicy); + }, [showAdvancedPolicy]); + useEffect(() => { if (!routeState && locationRouteState) { setRouteState(locationRouteState); @@ -245,6 +251,18 @@ export const PolicyDetails = React.memo(() => { + + + + + + + + {showAdvancedPolicy && } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 1da832fb081ef..215851fb28152 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -11,6 +11,7 @@ import { EuiRadio, EuiSwitch, EuiTitle, + EuiText, EuiSpacer, htmlIdGenerator, EuiCallOut, @@ -18,6 +19,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { cloneDeep } from 'lodash'; import { APP_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; @@ -26,8 +28,8 @@ import { OS, MalwareProtectionOSes } from '../../../types'; import { ConfigForm } from '../config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { clone } from '../../../models/policy_details_config'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; +import { popupVersionsMap } from './popup_options_to_versions'; const ProtectionRadioGroup = styled.div` display: flex; @@ -48,7 +50,7 @@ const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: const handleRadioChange = useCallback(() => { if (policyDetailsConfig) { - const newPayload = clone(policyDetailsConfig); + const newPayload = cloneDeep(policyDetailsConfig); for (const os of OSes) { newPayload[os][protection].mode = id; if (id === ProtectionModes.prevent) { @@ -83,6 +85,25 @@ const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: ProtectionRadio.displayName = 'ProtectionRadio'; +const SupportedVersionNotice = ({ optionName }: { optionName: string }) => { + const version = popupVersionsMap.get(optionName); + if (!version) { + return null; + } + + return ( + + + + + + ); +}; + /** The Malware Protections form for policy details * which will configure for all relevant OSes. */ @@ -120,7 +141,7 @@ export const MalwareProtections = React.memo(() => { const handleSwitchChange = useCallback( (event) => { if (policyDetailsConfig) { - const newPayload = clone(policyDetailsConfig); + const newPayload = cloneDeep(policyDetailsConfig); if (event.target.checked === false) { for (const os of OSes) { newPayload[os][protection].mode = ProtectionModes.off; @@ -144,7 +165,7 @@ export const MalwareProtections = React.memo(() => { const handleUserNotificationCheckbox = useCallback( (event) => { if (policyDetailsConfig) { - const newPayload = clone(policyDetailsConfig); + const newPayload = cloneDeep(policyDetailsConfig); for (const os of OSes) { newPayload[os].popup[protection].enabled = event.target.checked; } @@ -189,14 +210,15 @@ export const MalwareProtections = React.memo(() => { />

    + + ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts new file mode 100644 index 0000000000000..d4c7d0102ebd4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const popupVersions: Array<[string, string]> = [['malware', '7.11+']]; + +export const popupVersionsMap: ReadonlyMap = new Map(popupVersions); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 5a3b4ec384686..a292ec3e1a119 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -131,7 +131,7 @@ const OverviewComponent: React.FC = ({ ( - { indices: defaultIndicesName, onlyCheckIfIndicesExist: false }, + { indices: defaultIndicesName, onlyCheckIfIndicesExist: true }, { strategy: 'securitySolutionIndexFields', } diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts index 6fb84eaf7fda6..837d824db8748 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts @@ -78,8 +78,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { */ async eventsWithEntityIDAndCategory( entityID: string, - category: string, - after?: string + category: string ): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { const events = entityID === metadata.entityIDs.origin diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/get_ui_settings.ts b/x-pack/plugins/security_solution/public/resolver/mocks/ui_setting.ts similarity index 79% rename from x-pack/plugins/security_solution/public/resolver/mocks/get_ui_settings.ts rename to x-pack/plugins/security_solution/public/resolver/mocks/ui_setting.ts index ab1a5c86859ac..4d173cd270cb8 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/get_ui_settings.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/ui_setting.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export function getUiSettings(key: string): string | undefined { +/** + * A mock for Kibana UI settings. + */ +export function uiSetting(key: string): string | undefined { if (key === 'dateFormat') { return 'MMM D, YYYY @ HH:mm:ss.SSS'; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index 66a32ba29cd74..26a5f8555a81b 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -8,16 +8,25 @@ import { DataAction } from './data/action'; /** * When the user wants to bring a node front-and-center on the map. + * @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties` */ interface UserBroughtNodeIntoView { + /** + * @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties` + */ readonly type: 'userBroughtNodeIntoView'; + /** + * @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties` + */ readonly payload: { /** * Used to identify the node that should be brought into view. + * @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties` */ readonly nodeID: string; /** * The time (since epoch in milliseconds) when the action was dispatched. + * @deprecated Nodes are brought into view upon selection instead. See `appReceivedNewExternalProperties` */ readonly time: number; }; diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index aa04221361de0..23b651c262cba 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -34,7 +34,7 @@ expect.extend({ expected: T ): Promise<{ pass: boolean; message: () => string }> { // Used in printing out the pass or fail message - const matcherName = 'toSometimesYieldEqualTo'; + const matcherName = 'toYieldEqualTo'; const options: jest.MatcherHintOptions = { comment: 'deep equality with any yielded value', isNot: this.isNot, @@ -100,9 +100,9 @@ expect.extend({ expected: T ): Promise<{ pass: boolean; message: () => string }> { // Used in printing out the pass or fail message - const matcherName = 'toSometimesYieldEqualTo'; + const matcherName = 'toYieldObjectEqualTo'; const options: jest.MatcherHintOptions = { - comment: 'deep equality with any yielded value', + comment: 'subset equality with any yielded value', isNot: this.isNot, promise: this.promise, }; diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 2a399b6844bd7..2a538620dce0b 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -16,7 +16,7 @@ import { MockResolver } from './mock_resolver'; import { ResolverState, DataAccessLayer, SpyMiddleware, SideEffectSimulator } from '../../types'; import { ResolverAction } from '../../store/actions'; import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory'; -import { getUiSettings } from '../../mocks/get_ui_settings'; +import { uiSetting } from '../../mocks/ui_setting'; /** * Test a Resolver instance using jest, enzyme, and a mock data layer. @@ -62,6 +62,13 @@ export class Simulator { return selector; } + /** + * The simulator returns enzyme `ReactWrapper`s from various methods. Use this predicate to determine if they are DOM nodes. + */ + public static isDOM(wrapper: ReactWrapper): boolean { + return typeof wrapper.type() === 'string'; + } + constructor({ dataAccessLayer, resolverComponentInstanceID, @@ -110,7 +117,7 @@ export class Simulator { // Used for `KibanaContextProvider` const coreStart = coreMock.createStart(); - coreStart.uiSettings.get.mockImplementation(getUiSettings); + coreStart.uiSettings.get.mockImplementation(uiSetting); this.sideEffectSimulator = sideEffectSimulatorFactory(); @@ -190,7 +197,7 @@ export class Simulator { * After 10 times, quit. * Use this to continually check a value. See `toYieldEqualTo`. */ - public async *map(mapper: () => R): AsyncIterable { + public async *map(mapper: (() => Promise) | (() => R)): AsyncIterable { let timeoutCount = 0; while (timeoutCount < 10) { timeoutCount++; @@ -267,6 +274,20 @@ export class Simulator { this.sideEffectSimulator.controls.provideAnimationFrame(); } + /** + * The last value written to the clipboard via the `SideEffectors`. + */ + public get clipboardText(): string { + return this.sideEffectSimulator.controls.clipboardText; + } + + /** + * Call this to resolve the promise returned by the `SideEffectors` `writeText` method (which in production points to `navigator.clipboard.writeText`. + */ + confirmTextWrittenToClipboard(): void { + this.sideEffectSimulator.controls.confirmTextWrittenToClipboard(); + } + /** * The 'search' part of the URL. */ @@ -296,13 +317,36 @@ export class Simulator { return this.domNodes(`[data-test-subj="${selector}"]`); } + /** + * Given a `ReactWrapper`, returns a wrapper containing immediately following `dd` siblings. + * `subject` must contain just 1 element. + */ + public descriptionDetails(subject: ReactWrapper): ReactWrapper { + // find the associated DOM nodes, then return an enzyme wrapper that only contains those. + const subjectNode = subject.getDOMNode(); + let current = subjectNode.nextElementSibling; + const associated: Set = new Set(); + // Multiple `dt`s can be associated with a set of `dd`s. Skip immediately following `dt`s. + while (current !== null && current.nodeName === 'DT') { + current = current.nextElementSibling; + } + while (current !== null && current.nodeName === 'DD') { + associated.add(current); + current = current.nextElementSibling; + } + return subject + .closest('dl') + .find('dd') + .filterWhere((candidate) => { + return associated.has(candidate.getDOMNode()); + }); + } + /** * Return DOM nodes that match `enzymeSelector`. */ private domNodes(enzymeSelector: string): ReactWrapper { - return this.wrapper - .find(enzymeSelector) - .filterWhere((wrapper) => typeof wrapper.type() === 'string'); + return this.wrapper.find(enzymeSelector).filterWhere(Simulator.isDOM); } /** @@ -331,7 +375,7 @@ export class Simulator { * Resolve the wrapper returned by `wrapperFactory` only once it has at least 1 element in it. */ public async resolveWrapper( - wrapperFactory: () => ReactWrapper, + wrapperFactory: (() => Promise) | (() => ReactWrapper), predicate: (wrapper: ReactWrapper) => boolean = (wrapper) => wrapper.length > 0 ): Promise { for await (const wrapper of this.map(wrapperFactory)) { diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index fb57f85639e33..7129e3a47120a 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -490,9 +490,26 @@ export interface SideEffectors { * A function which returns the time since epoch in milliseconds. Injected because mocking Date is tedious. */ timestamp: () => number; + /** + * Use instead of `window.requestAnimationFrame` + **/ requestAnimationFrame: typeof window.requestAnimationFrame; + /** + * Use instead of `window.cancelAnimationFrame` + **/ cancelAnimationFrame: typeof window.cancelAnimationFrame; + /** + * Use instead of the `ResizeObserver` global. + */ ResizeObserver: ResizeObserverConstructor; + /** + * Use this instead of the Clipboard API's `writeText` method. + */ + writeTextToClipboard(text: string): Promise; + /** + * Use this instead of `Element.prototype.getBoundingClientRect` . + */ + getBoundingClientRect(element: Element): DOMRect; } export interface SideEffectSimulator { @@ -512,6 +529,16 @@ export interface SideEffectSimulator { * Trigger `ResizeObserver` callbacks for `element` and update the mocked value for `getBoundingClientRect`. */ simulateElementResize: (element: Element, contentRect: DOMRect) => void; + + /** + * Get the most recently written clipboard text. This is only updated when `confirmTextWrittenToClipboard` is called. + */ + clipboardText: string; + + /** + * Call this to resolve the promise returned by `writeText`. + */ + confirmTextWrittenToClipboard: () => void; }; /** * Mocked `SideEffectors`. diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index c781832dc8a3b..7739d81269180 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -165,10 +165,7 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', ).toYieldEqualTo({ treeCount: 1, nodesOwnedByTrees: 3 }); }); - it(`should show links to the 3 nodes (with icons) in the node list.`, async () => { - await expect( - simulator.map(() => simulator.testSubject('resolver:node-list:node-link:title').length) - ).toYieldEqualTo(3); + it(`should show links to the 3 nodes in the node list.`, async () => { await expect( simulator.map(() => simulator.testSubject('resolver:node-list:node-link:title').length) ).toYieldEqualTo(3); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/generated_text.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx rename to x-pack/plugins/security_solution/public/resolver/view/generated_text.tsx index a20498cbfb67b..61a12fa33cc9d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/generated_text.tsx @@ -4,33 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; /* eslint-disable react/display-name */ - -import { EuiCode } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import React, { memo } from 'react'; - -/** - * Text to use in place of an undefined timestamp value - */ - -export const noTimestampRetrievedText = i18n.translate( - 'xpack.securitySolution.enpdoint.resolver.panelutils.noTimestampRetrieved', - { - defaultMessage: 'No timestamp retrieved', - } -); - -/** - * A bold version of EuiCode to display certain titles with - */ -export const BoldCode = styled(EuiCode)` - &.euiCodeBlock code.euiCodeBlock__code { - font-weight: 900; - } -`; - /** * A component that renders an element with breaking opportunities (``s) * spliced into text children at word boundaries. @@ -61,12 +36,3 @@ export const GeneratedText = React.memo(function ({ children }) { }); } }); - -/** - * A component to keep time representations in blocks so they don't wrap - * and look bad. - */ -export const StyledTime = memo(styled('time')` - display: inline-block; - text-align: start; -`); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 9d72af3109564..3b3651ec2558a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -29,6 +29,20 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and secondChild: string; }; + /** + * These are the details we expect to see in the node detail view when the origin is selected. + */ + const originEventDetailEntries: ReadonlyMap = new Map([ + ['@timestamp', 'Sep 23, 2020 @ 08:25:32.316'], + ['process.executable', 'executable'], + ['process.pid', '0'], + ['user.name', 'user.name'], + ['user.domain', 'user.domain'], + ['process.parent.pid', '0'], + ['process.hash.md5', 'hash.md5'], + ['process.args', 'args'], + ]); + beforeEach(() => { // create a mock data access layer const { @@ -86,16 +100,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and ).toYieldEqualTo({ title: 'c.ext', titleIcon: 'Running Process', - detailEntries: [ - ['@timestamp', 'Sep 23, 2020 @ 08:25:32.316'], - ['process.executable', 'executable'], - ['process.pid', '0'], - ['user.name', 'user.name'], - ['user.domain', 'user.domain'], - ['process.parent.pid', '0'], - ['process.hash.md5', 'hash.md5'], - ['process.args', 'args'], - ], + detailEntries: [...originEventDetailEntries], }); }); it('should have breaking opportunities (s) in node titles to allow wrapping', async () => { @@ -111,16 +116,46 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and wordBreaks: 2, }); }); - it('should allow all node details to be copied', async () => { - const copyableFields = await simulator().resolve('resolver:panel:copyable-field'); - copyableFields?.map((copyableField) => { - copyableField.simulate('mouseenter'); - simulator().testSubject('resolver:panel:clipboard').last().simulate('click'); - expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text()); - copyableField.simulate('mouseleave'); - }); - }); + /** + * These tests use a statically defined map of fields and expected values. The test finds the `dt` for each field and then finds the related `dd`s. From there it finds a special 'hover area' (via `data-test-subj`) and simulates a `mouseenter` on it. This is because the feature work by adding event listeners to `div`s. There is no way for the user to know that the `div`s are interactable. + + * Finally the test clicks a button and checks that the clipboard was written to. + */ + describe.each([...originEventDetailEntries])( + 'when the user hovers over the description for the field (%p) with their mouse', + (fieldTitleText, value) => { + beforeEach(async () => { + const dt = await simulator().resolveWrapper(() => { + return simulator() + .testSubject('resolver:node-detail:entry-title') + .filterWhere((title) => title.text() === fieldTitleText); + }); + + expect(dt).toHaveLength(1); + + const copyableFieldHoverArea = simulator() + .descriptionDetails(dt!) + // The copyable field popup does not use a button as a trigger. It is instead triggered by mouse interaction with this `div`. + .find(`[data-test-subj="resolver:panel:copyable-field-hover-area"]`) + .filterWhere(Simulator.isDOM); + + expect(copyableFieldHoverArea).toHaveLength(1); + copyableFieldHoverArea!.simulate('mouseenter'); + }); + describe('and when they click the copy-to-clipboard button', () => { + beforeEach(async () => { + const copyButton = await simulator().resolve('resolver:panel:clipboard'); + expect(copyButton).toHaveLength(1); + copyButton!.simulate('click'); + simulator().confirmTextWrittenToClipboard(); + }); + it(`should write ${value} to the clipboard`, async () => { + await expect(simulator().map(() => simulator().clipboardText)).toYieldEqualTo(value); + }); + }); + } + ); }); const queryStringWithFirstChildSelected = urlSearch(resolverComponentInstanceID, { @@ -160,23 +195,43 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and it('should have 3 nodes (with icons) in the node list', async () => { await expect( - simulator().map(() => simulator().testSubject('resolver:node-list:node-link:title').length) - ).toYieldEqualTo(3); - await expect( - simulator().map(() => simulator().testSubject('resolver:node-list:node-link:icon').length) - ).toYieldEqualTo(3); + simulator().map(() => { + return { + titleCount: simulator().testSubject('resolver:node-list:node-link:title').length, + iconCount: simulator().testSubject('resolver:node-list:node-link:icon').length, + }; + }) + ).toYieldEqualTo({ titleCount: 3, iconCount: 3 }); }); - it('should be able to copy the timestamps for all 3 nodes', async () => { - const copyableFields = await simulator().resolve('resolver:panel:copyable-field'); + describe('when the user hovers over the timestamp for "c.ext" with their mouse', () => { + beforeEach(async () => { + const cExtHoverArea = await simulator().resolveWrapper(async () => { + const nodeLinkTitles = await simulator().resolve('resolver:node-list:node-link:title'); - expect(copyableFields?.length).toBe(3); + expect(nodeLinkTitles).toHaveLength(3); - copyableFields?.map((copyableField) => { - copyableField.simulate('mouseenter'); - simulator().testSubject('resolver:panel:clipboard').last().simulate('click'); - expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text()); - copyableField.simulate('mouseleave'); + return ( + nodeLinkTitles! + .filterWhere((linkTitle) => linkTitle.text() === 'c.ext') + // Find the parent `tr` and the find all hover areas in that TR. The test assumes that all cells in a row are associated. + .closest('tr') + // The copyable field popup does not use a button as a trigger. It is instead triggered by mouse interaction with this `div`. + .find('[data-test-subj="resolver:panel:copyable-field-hover-area"]') + .filterWhere(Simulator.isDOM) + ); + }); + cExtHoverArea!.simulate('mouseenter'); + }); + describe('and when the user clicks the copy-to-clipboard button', () => { + beforeEach(async () => { + (await simulator().resolve('resolver:panel:clipboard'))!.simulate('click'); + simulator().confirmTextWrittenToClipboard(); + }); + const expected = 'Sep 23, 2020 @ 08:25:32.316'; + it(`should write "${expected}" to the clipboard`, async () => { + await expect(simulator().map(() => simulator().clipboardText)).toYieldEqualTo(expected); + }); }); }); @@ -191,16 +246,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and it('should show the details for the first node', async () => { await expect( simulator().map(() => simulator().nodeDetailDescriptionListEntries()) - ).toYieldEqualTo([ - ['@timestamp', 'Sep 23, 2020 @ 08:25:32.316'], - ['process.executable', 'executable'], - ['process.pid', '0'], - ['user.name', 'user.name'], - ['user.domain', 'user.domain'], - ['process.parent.pid', '0'], - ['process.hash.md5', 'hash.md5'], - ['process.args', 'args'], - ]); + ).toYieldEqualTo([...originEventDetailEntries]); }); it("should have the first node's ID in the query string", async () => { await expect(simulator().map(() => simulator().historyLocationSearch)).toYieldEqualTo( @@ -278,16 +324,40 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and simulator().map(() => simulator().testSubject('resolver:panel:event-detail').length) ).toYieldEqualTo(1); }); - it('should allow all fields to be copied', async () => { - const copyableFields = await simulator().resolve('resolver:panel:copyable-field'); - - copyableFields?.map((copyableField) => { - copyableField.simulate('mouseenter'); - simulator().testSubject('resolver:panel:clipboard').last().simulate('click'); - expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text()); - copyableField.simulate('mouseleave'); - }); - }); + describe.each([['user.domain', 'user.domain']])( + 'when the user hovers over the description for the field "%p"', + (fieldName, expectedValue) => { + beforeEach(async () => { + const fieldHoverArea = await simulator().resolveWrapper(async () => { + const dt = ( + await simulator().resolve('resolver:panel:event-detail:event-field-title') + )?.filterWhere((title) => title.text() === fieldName); + return ( + simulator() + .descriptionDetails(dt!) + // The copyable field popup does not use a button as a trigger. It is instead triggered by mouse interaction with this `div`. + .find(`[data-test-subj="resolver:panel:copyable-field-hover-area"]`) + .filterWhere(Simulator.isDOM) + ); + }); + expect(fieldHoverArea).toBeTruthy(); + fieldHoverArea?.simulate('mouseenter'); + }); + describe('when the user clicks on the clipboard button', () => { + beforeEach(async () => { + const button = await simulator().resolve('resolver:panel:clipboard'); + expect(button).toBeTruthy(); + button!.simulate('click'); + simulator().confirmTextWrittenToClipboard(); + }); + it(`should write ${expectedValue} to the clipboard`, async () => { + await expect(simulator().map(() => simulator().clipboardText)).toYieldEqualTo( + expectedValue + ); + }); + }); + } + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx index f6a585ea566bb..6a1667a839548 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx @@ -9,10 +9,11 @@ import { EuiToolTip, EuiButtonIcon, EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import React, { memo, useState, useCallback } from 'react'; +import React, { memo, useState, useCallback, useContext, useMemo } from 'react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useColors } from '../use_colors'; import { StyledPanel } from '../styles'; +import { SideEffectContext } from '../side_effect_context'; interface StyledCopyableField { readonly backgroundColor: string; @@ -48,39 +49,41 @@ export const CopyablePanelField = memo( const onMouseEnter = () => setIsOpen(true); const onMouseLeave = () => setIsOpen(false); - const ButtonContent = memo(() => ( - - {content} - - )); + const hoverArea = useMemo( + () => ( + + {content} + + ), + [content, copyableFieldBackground, linkColor] + ); - const onClick = useCallback( - async (event: React.MouseEvent) => { - try { - await navigator.clipboard.writeText(textToCopy); - } catch (error) { - if (toasts) { - toasts.addError(error, { - title: i18n.translate('xpack.securitySolution.resolver.panel.copyFailureTitle', { - defaultMessage: 'Copy Failure', - }), - }); - } + const { writeTextToClipboard } = useContext(SideEffectContext); + + const onClick = useCallback(async () => { + try { + await writeTextToClipboard(textToCopy); + } catch (error) { + if (toasts) { + toasts.addError(error, { + title: i18n.translate('xpack.securitySolution.resolver.panel.copyFailureTitle', { + defaultMessage: 'Copy Failure', + }), + }); } - }, - [textToCopy, toasts] - ); + } + }, [textToCopy, toasts, writeTextToClipboard]); return (
    } + button={hoverArea} closePopover={onMouseLeave} hasArrow={false} isOpen={isOpen} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index e5569b30abb9d..4936cf0cbb80e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -15,12 +15,8 @@ import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from ' import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { StyledPanel } from '../styles'; -import { - BoldCode, - StyledTime, - GeneratedText, - noTimestampRetrievedText, -} from './panel_content_utilities'; +import { BoldCode, StyledTime } from './styles'; +import { GeneratedText } from '../generated_text'; import { CopyablePanelField } from './copyable_panel_field'; import { Breadcrumbs } from './breadcrumbs'; import * as eventModel from '../../../../common/endpoint/models/event'; @@ -97,7 +93,12 @@ const EventDetailContents = memo(function ({ processEvent: SafeResolverEvent | null; }) { const timestamp = eventModel.timestampSafeVersion(event); - const formattedDate = useFormattedDate(timestamp) || noTimestampRetrievedText; + const formattedDate = + useFormattedDate(timestamp) || + i18n.translate('xpack.securitySolution.enpdoint.resolver.panelutils.noTimestampRetrieved', { + defaultMessage: 'No timestamp retrieved', + }); + const nodeName = processEvent ? eventModel.processNameSafeVersion(processEvent) : null; return ( @@ -155,15 +156,20 @@ function EventDetailFields({ event }: { event: SafeResolverEvent }) { const section = { // Group the fields by their top-level namespace namespace: {key}, - descriptions: deepObjectEntries(value).map(([path, fieldValue]) => ({ - title: {path.join('.')}, - description: ( - {String(fieldValue)}} - /> - ), - })), + descriptions: deepObjectEntries(value).map(([path, fieldValue]) => { + // The field name is the 'namespace' key as well as the rest of the path, joined with '.' + const fieldName = [key, ...path].join('.'); + + return { + title: {fieldName}, + description: ( + {String(fieldValue)}} + /> + ), + }; + }), }; returnValue.push(section); } @@ -187,7 +193,10 @@ function EventDetailFields({ event }: { event: SafeResolverEvent }) { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx index c7d4f8632659b..5675e29fc2bc1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx @@ -16,7 +16,7 @@ import { EuiDescriptionListProps } from '@elastic/eui/src/components/description import { StyledDescriptionList, StyledTitle } from './styles'; import * as selectors from '../../store/selectors'; import * as eventModel from '../../../../common/endpoint/models/event'; -import { GeneratedText } from './panel_content_utilities'; +import { GeneratedText } from '../generated_text'; import { CopyablePanelField } from './copyable_panel_field'; import { Breadcrumbs } from './breadcrumbs'; import { processPath, processPID } from '../../models/process_event'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index 17e91902d0c96..c9648c6f562e5 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React, { memo, useCallback, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -18,7 +20,7 @@ import { import { useSelector } from 'react-redux'; import { FormattedMessage } from '@kbn/i18n/react'; import { StyledPanel } from '../styles'; -import { BoldCode, noTimestampRetrievedText, StyledTime } from './panel_content_utilities'; +import { BoldCode, StyledTime } from './styles'; import { Breadcrumbs } from './breadcrumbs'; import * as eventModel from '../../../../common/endpoint/models/event'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; @@ -99,8 +101,6 @@ export const NodeEventsInCategory = memo(function ({ ); }); -NodeEventsInCategory.displayName = 'NodeEventsInCategory'; - /** * Rendered for each event in the list. */ @@ -114,7 +114,11 @@ const NodeEventsListItem = memo(function ({ eventCategory: string; }) { const timestamp = eventModel.eventTimestamp(event); - const date = useFormattedDate(timestamp) || noTimestampRetrievedText; + const date = + useFormattedDate(timestamp) || + i18n.translate('xpack.securitySolution.enpdoint.resolver.panelutils.noTimestampRetrieved', { + defaultMessage: 'No timestamp retrieved', + }); const linkProps = useLinkProps({ panelView: 'eventDetail', panelParameters: { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx index 9ef72c414bb63..e53cd2cc0860d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx @@ -38,17 +38,14 @@ import { LimitWarning } from '../limit_warnings'; import { ResolverState } from '../../types'; import { useLinkProps } from '../use_link_props'; import { useColors } from '../use_colors'; -import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { ResolverAction } from '../../store/actions'; import { useFormattedDate } from './use_formatted_date'; -import { getEmptyTagValue } from '../../../common/components/empty_value'; import { CopyablePanelField } from './copyable_panel_field'; interface ProcessTableView { name?: string; timestamp?: Date; nodeID: string; - event: SafeResolverEvent; } /** @@ -68,7 +65,7 @@ export const NodeList = memo(() => { sortable: true, truncateText: true, render(name: string | undefined, item: ProcessTableView) { - return ; + return ; }, }, { @@ -101,7 +98,6 @@ export const NodeList = memo(() => { name, timestamp: eventModel.timestampAsDateSafeVersion(processEvent), nodeID, - event: processEvent, }); } } @@ -111,7 +107,7 @@ export const NodeList = memo(() => { const numberOfProcesses = processTableView.length; - const crumbs = useMemo(() => { + const breadcrumbs = useMemo(() => { return [ { text: i18n.translate('xpack.securitySolution.resolver.panel.nodeList.title', { @@ -127,7 +123,7 @@ export const NodeList = memo(() => { const rowProps = useMemo(() => ({ 'data-test-subj': 'resolver:node-list:item' }), []); return ( - + {showWarning && } @@ -141,15 +137,7 @@ export const NodeList = memo(() => { ); }); -function NodeDetailLink({ - name, - nodeID, - event, -}: { - name?: string; - nodeID: string; - event: SafeResolverEvent; -}) { +function NodeDetailLink({ name, nodeID }: { name?: string; nodeID: string }) { const isOrigin = useSelector((state: ResolverState) => { return selectors.originID(state) === nodeID; }); @@ -175,7 +163,7 @@ function NodeDetailLink({ ); return ( - {name === '' ? ( + {name === undefined ? ( {i18n.translate( 'xpack.securitySolution.endpoint.resolver.panel.table.row.valueMissingDescription', @@ -218,6 +206,6 @@ const NodeDetailTimestamp = memo(({ eventDate }: { eventDate: Date | undefined } return formattedDate ? ( ) : ( - getEmptyTagValue() + {'—'} ); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx index 03826dd38397b..6f9d4bb600fde 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiCode } from '@elastic/eui'; + /* eslint-disable no-duplicate-imports */ import { EuiBreadcrumbs } from '@elastic/eui'; @@ -89,3 +91,21 @@ export const StyledLabelContainer = styled.div` white-space: nowrap; } `; + +/** + * A bold version of EuiCode to display certain titles with + */ +export const BoldCode = styled(EuiCode)` + &.euiCodeBlock code.euiCodeBlock__code { + font-weight: 900; + } +`; + +/** + * A component to keep time representations in blocks so they don't wrap + * and look bad. + */ +export const StyledTime = styled('time')` + display: inline-block; + text-align: start; +`; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.test.tsx index c08c3b370558b..647f7c75d0298 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.test.tsx @@ -4,103 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ +import { mount } from 'enzyme'; + import React from 'react'; -import { render, RenderResult } from '@testing-library/react'; import { useFormattedDate } from './use_formatted_date'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; -import { getUiSettings } from '../../mocks/get_ui_settings'; +import { uiSetting } from '../../mocks/ui_setting'; -describe('useFormattedDate', () => { - let element: HTMLElement; - const testID = 'formattedDate'; - let reactRenderResult: ( - date: ConstructorParameters[0] | Date | undefined - ) => RenderResult; +describe(`useFormattedDate, when the "dateFormat" UI setting is "${uiSetting( + 'dateFormat' +)}" and the "dateFormat:tz" setting is "${uiSetting('dateFormat:tz')}"`, () => { + let formattedDate: (date: ConstructorParameters[0] | Date | undefined) => string; beforeEach(async () => { const mockCoreStart = coreMock.createStart(); - mockCoreStart.uiSettings.get.mockImplementation(getUiSettings); + mockCoreStart.uiSettings.get.mockImplementation(uiSetting); function Test({ date }: { date: ConstructorParameters[0] | Date | undefined }) { - const formattedDate = useFormattedDate(date); - return
    {formattedDate}
    ; + return <>{useFormattedDate(date)}; } - reactRenderResult = ( - date: ConstructorParameters[0] | Date | undefined - ): RenderResult => - render( + formattedDate = (date: ConstructorParameters[0] | Date | undefined): string => + mount( - ); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('when the provided date is undefined', () => { - it('should return undefined', async () => { - const { findByTestId } = reactRenderResult(undefined); - element = await findByTestId(testID); - - expect(element).toBeEmptyDOMElement(); - }); - }); - - describe('when the provided date is empty', () => { - it('should return undefined', async () => { - const { findByTestId } = reactRenderResult(''); - element = await findByTestId(testID); - - expect(element).toBeEmptyDOMElement(); - }); - }); - - describe('when the provided date is an invalid date', () => { - it('should return the string invalid date', async () => { - const { findByTestId } = reactRenderResult('randomString'); - element = await findByTestId(testID); - - expect(element).toHaveTextContent('Invalid Date'); - }); - }); - - describe('when the provided date is a stringified unix timestamp', () => { - it('should return the string invalid date', async () => { - const { findByTestId } = reactRenderResult('1600863932316'); - element = await findByTestId(testID); - - expect(element).toHaveTextContent('Invalid Date'); - }); - }); - - describe('when the provided date is a valid numerical timestamp', () => { - it('should return the string invalid date', async () => { - const { findByTestId } = reactRenderResult(1600863932316); - element = await findByTestId(testID); - - expect(element).toHaveTextContent('Sep 23, 2020 @ 08:25:32.316'); - }); - }); - - describe('when the provided date is a date string', () => { - it('should return the string invalid date', async () => { - const { findByTestId } = reactRenderResult('2020-09-23T12:25:32Z'); - element = await findByTestId(testID); - - expect(element).toHaveTextContent('Sep 23, 2020 @ 08:25:32.000'); - }); - }); - - describe('when the provided date is a valid date', () => { - it('should return the string invalid date', async () => { - const validDate = new Date(1600863932316); - const { findByTestId } = reactRenderResult(validDate); - element = await findByTestId(testID); - - expect(element).toHaveTextContent('Sep 23, 2020 @ 08:25:32.316'); - }); + ).text(); + }); + + it.each([ + ['randomString', 'an invalid string', 'Invalid Date'], + [ + '1600863932316', + "a string that does't match the configured time format settings", + 'Invalid Date', + ], + [1600863932316, 'a valid unix timestamp', 'Sep 23, 2020 @ 08:25:32.316'], + [undefined, 'undefined', ''], + ['', 'an empty string', ''], + [ + '2020-09-23T12:25:32Z', + 'a string that conforms to the specified format', + 'Sep 23, 2020 @ 08:25:32.000', + ], + [new Date(1600863932316), 'a defined Date object', 'Sep 23, 2020 @ 08:25:32.316'], + ])('when the provided date is %p (%s) it should return %p', (value, _explanation, expected) => { + expect(formattedDate(value)).toBe(expected); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/side_effect_context.ts b/x-pack/plugins/security_solution/public/resolver/view/side_effect_context.ts index ab7f41d815026..71b054948160e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/side_effect_context.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/side_effect_context.ts @@ -19,6 +19,12 @@ const sideEffectors: SideEffectors = { return window.cancelAnimationFrame(...args); }, ResizeObserver, + writeTextToClipboard(text: string): Promise { + return navigator.clipboard.writeText(text); + }, + getBoundingClientRect(element: Element): DOMRect { + return element.getBoundingClientRect(); + }, }; /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts b/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts index 8517459b8aba3..84da2824962d6 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts @@ -37,7 +37,7 @@ export const sideEffectSimulatorFactory: () => SideEffectSimulator = () => { /** * Get the simulate `DOMRect` for `element`. */ - const contentRectForElement: (target: Element) => DOMRect = (target) => { + const getBoundingClientRect: (target: Element) => DOMRect = (target) => { if (contentRects.has(target)) { return contentRects.get(target)!; } @@ -58,26 +58,33 @@ export const sideEffectSimulatorFactory: () => SideEffectSimulator = () => { }; /** - * Change `Element.prototype.getBoundingClientRect` to return our faked values. + * Last value written to the clipboard, of '' if no text has been written. Returned by the `controls`. */ - jest - .spyOn(Element.prototype, 'getBoundingClientRect') - .mockImplementation(function (this: Element) { - return contentRectForElement(this); - }); + let clipboardText: string = ''; // the `readText` method of the Clipboard API returns an empty string if the clipboard is empty. + + function confirmTextWrittenToClipboard() { + const next = clipboardWriteTextQueue.shift(); + if (next) { + const [text, resolve] = next; + clipboardText = text; + resolve(); + } + } /** - * Mock the global writeText method as it is not available in jsDOM and alows us to track what was copied + * Queue of `text` waiting to be written to the clipboard. Calling `resolve` will resolve the promise returned by the mock `writeTextToClipboard` method. */ - const MockClipboard: Clipboard = { - writeText: jest.fn(), - readText: jest.fn(), - addEventListener: jest.fn(), - dispatchEvent: jest.fn(), - removeEventListener: jest.fn(), - }; - // @ts-ignore navigator doesn't natively exist on global - global.navigator.clipboard = MockClipboard; + const clipboardWriteTextQueue: Array<[text: string, resolve: () => void]> = []; + + /** + * Mock `writeText` method of the `Clipboard` API. + */ + function writeTextToClipboard(text: string): Promise { + return new Promise((resolve) => { + clipboardWriteTextQueue.push([text, resolve]); + }); + } + /** * A mock implementation of `ResizeObserver` that works with our fake `getBoundingClientRect` and `simulateElementResize` */ @@ -171,12 +178,20 @@ export const sideEffectSimulatorFactory: () => SideEffectSimulator = () => { }, simulateElementResize, + + get clipboardText() { + return clipboardText; + }, + + confirmTextWrittenToClipboard, }, mock: { requestAnimationFrame, cancelAnimationFrame, timestamp, ResizeObserver: MockResizeObserver, + writeTextToClipboard, + getBoundingClientRect, }, }; return retval; diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index bf72a52559cbd..35cf2c36d6627 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; -import { render, waitFor, RenderResult, fireEvent } from '@testing-library/react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { useCamera, useAutoUpdatingClientRect } from './use_camera'; +// Extend jest with a custom matcher +import '../test_utilities/extend_jest'; + +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { useCamera } from './use_camera'; import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; -import { Matrix3, ResolverStore, SideEffectSimulator } from '../types'; +import { Matrix3, ResolverStore, SideEffectors, SideEffectSimulator } from '../types'; import { SafeResolverEvent } from '../../../common/endpoint/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../models/vector2'; @@ -22,45 +24,120 @@ import { createStore } from 'redux'; import { resolverReducer } from '../store/reducer'; import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters'; import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; +import { act } from 'react-dom/test-utils'; describe('useCamera on an unpainted element', () => { - let element: HTMLElement; + /** Enzyme full DOM wrapper for the element the camera is attached to. */ + let element: ReactWrapper; + /** + * Enzyme full DOM wrapper for the alternate element that the camera can be attached to. Used for testing that the `ResizeObserver` attaches itself to the latest `ref`. + */ + let alternateElement: ReactWrapper; + /** + * projection matrix returned by camera on last render. + */ let projectionMatrix: Matrix3; + /** + * A `data-test-subj` ID used to identify the element the camera normally attaches to. + */ const testID = 'camera'; - let reactRenderResult: RenderResult; + /** + * A `data-test-subj` ID used to identify the element the camera alternatively attaches to. + */ + const alternateTestID = 'alternate'; + /** + * Returned by the legacy framework's render/mount function. + */ + let wrapper: ReactWrapper; let store: ResolverStore; let simulator: SideEffectSimulator; - beforeEach(async () => { - store = createStore(resolverReducer); - - const Test = function () { - const camera = useCamera(); - const { ref, onMouseDown } = camera; - projectionMatrix = camera.projectionMatrix; - return
    ; - }; + /** Used to find an element by the data-test-subj attribute. + */ + let domElementByTestSubj: (testSubj: string) => ReactWrapper; - simulator = sideEffectSimulatorFactory(); + /** + * Yield the result of `mapper` over and over, once per event-loop cycle. + * After 10 times, quit. + * Use this to continually check a value. See `toYieldEqualTo`. + */ + async function* map(mapper: () => R): AsyncIterable { + let timeoutCount = 0; + while (timeoutCount < 10) { + timeoutCount++; + yield mapper(); + await new Promise((resolve) => { + setTimeout(() => { + wrapper.update(); + resolve(); + }, 0); + }); + } + } - reactRenderResult = render( - - - + function TestWrapper({ + useSecondElement: useAlternateElement = false, + resolverStore, + sideEffectors, + }: { + /** + * Pass `true`, to attach the camera to an alternate element. Used to test that the `ResizeObserver` attaches itself to the latest `ref`. + */ + useSecondElement?: boolean; + resolverStore: ResolverStore; + sideEffectors: SideEffectors; + }) { + return ( + + + ); + } - const { findByTestId } = reactRenderResult; - element = await findByTestId(testID); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('should be usable in React', async () => { - expect(element).toBeInTheDocument(); + function Test({ + useAlternateElement = false, + }: { + /** + * Pass `true`, to attach the camera to an alternate element. Used to test that the `ResizeObserver` attaches itself to the latest `ref`. + */ + useAlternateElement?: boolean; + }) { + const camera = useCamera(); + const { ref, onMouseDown } = camera; + projectionMatrix = camera.projectionMatrix; + return useAlternateElement ? ( + <> +
    +
    + + ) : ( + <> +
    +
    + + ); + } + + beforeEach(async () => { + store = createStore(resolverReducer); + + simulator = sideEffectSimulatorFactory(); + + wrapper = mount(); + + domElementByTestSubj = (testSubj: string) => + wrapper + .find(`[data-test-subj="${testSubj}"]`) + // Omit React components that may be returned. + .filterWhere((item) => typeof item.type() === 'string'); + + element = domElementByTestSubj(testID); + + alternateElement = domElementByTestSubj(alternateTestID); }); - test('returns a projectionMatrix that changes everything to 0', () => { + it('returns a projectionMatrix that changes everything to 0', () => { expect(applyMatrix3([0, 0], projectionMatrix)).toEqual([0, 0]); }); describe('which has been resized to 800x600', () => { @@ -71,8 +148,8 @@ describe('useCamera on an unpainted element', () => { const centerX = width / 2 + leftMargin; const centerY = height / 2 + topMargin; beforeEach(async () => { - await waitFor(() => { - simulator.controls.simulateElementResize(element, { + act(() => { + simulator.controls.simulateElementResize(element.getDOMNode(), { width, height, left: leftMargin, @@ -87,73 +164,82 @@ describe('useCamera on an unpainted element', () => { }); }); }); - test('should observe all resize reference changes', async () => { - const wrapper: FunctionComponent = ({ children }) => ( - - {children} - - ); - - const { result } = renderHook(() => useAutoUpdatingClientRect(), { wrapper }); - const resizeObserverSpy = jest.spyOn(simulator.mock.ResizeObserver.prototype, 'observe'); - - let [rect, ref] = result.current; - act(() => ref(element)); - expect(resizeObserverSpy).toHaveBeenCalledWith(element); - - const div = document.createElement('div'); - act(() => ref(div)); - expect(resizeObserverSpy).toHaveBeenCalledWith(div); - - [rect, ref] = result.current; - expect(rect?.width).toBe(0); - }); - test('provides a projection matrix that inverts the y axis and translates 400,300 (center of the element)', () => { - expect(applyMatrix3([0, 0], projectionMatrix)).toEqual([400, 300]); + it('provides a projection matrix that inverts the y axis and translates 400,300 (center of the element)', () => { + expect(map(() => applyMatrix3([0, 0], projectionMatrix))).toYieldEqualTo([400, 300]); }); describe('when the user presses the mousedown button in the middle of the element', () => { beforeEach(() => { - fireEvent.mouseDown(element, { + element.simulate('mousedown', { clientX: centerX, clientY: centerY, }); }); describe('when the user moves the mouse 50 pixels to the right', () => { beforeEach(() => { - fireEvent.mouseMove(element, { + element.simulate('mousemove', { clientX: centerX + 50, clientY: centerY, }); }); it('should project [0, 0] in world corrdinates 50 pixels to the right of the center of the element', () => { - expect(applyMatrix3([0, 0], projectionMatrix)).toEqual([450, 300]); + expect(map(() => applyMatrix3([0, 0], projectionMatrix))).toYieldEqualTo([450, 300]); }); }); }); describe('when the user uses the mousewheel w/ ctrl held down', () => { beforeEach(() => { - fireEvent.wheel(element, { + element.simulate('wheel', { ctrlKey: true, deltaY: -10, deltaMode: 0, }); }); it('should zoom in', () => { - expect(projectionMatrix).toMatchInlineSnapshot(` - Array [ - 1.0292841801261479, - 0, - 400, - 0, - -1.0292841801261479, - 300, - 0, - 0, - 0, - ] - `); + expect(map(() => projectionMatrix)).toYieldEqualTo([ + 1.0292841801261479, + 0, + 400, + 0, + -1.0292841801261479, + 300, + 0, + 0, + 0, + ]); + }); + }); + + describe('when the element the camera is attached to is switched', () => { + beforeEach(() => { + wrapper.setProps({ + useAlternateElement: true, + }); + }); + describe('and when that element changes size to 1200x800', () => { + beforeEach(() => { + act(() => { + const alternateElementWidth = 1200; + const alternateElementHeight = 800; + simulator.controls.simulateElementResize(alternateElement.getDOMNode(), { + width: alternateElementWidth, + height: alternateElementHeight, + left: leftMargin, + top: topMargin, + right: leftMargin + alternateElementWidth, + bottom: topMargin + alternateElementHeight, + x: leftMargin, + y: topMargin, + toJSON() { + return this; + }, + }); + }); + }); + it('provides a projection matrix that inverts the y axis and translates 600,400', () => { + expect(map(() => applyMatrix3([0, 0], projectionMatrix))).toYieldEqualTo([600, 400]); + }); }); }); @@ -185,9 +271,7 @@ describe('useCamera on an unpainted element', () => { type: 'serverReturnedResolverData', payload: { result: tree, parameters: mockTreeFetcherParameters() }, }; - await waitFor(() => { - store.dispatch(serverResponseAction); - }); + store.dispatch(serverResponseAction); } else { throw new Error('failed to create tree'); } @@ -210,9 +294,7 @@ describe('useCamera on an unpainted element', () => { nodeID, }, }; - await waitFor(() => { - store.dispatch(cameraAction); - }); + store.dispatch(cameraAction); }); it('should request animation frames in a loop', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts b/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts index 661e038d04e32..c58b9f77d097d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts @@ -280,7 +280,10 @@ export function useCamera(): { * tracked. So if the element's position moves for some reason, be sure to * handle that. */ -export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { +function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { + // Access `getBoundingClientRect` via the `SideEffectContext` (for testing.) + const { getBoundingClientRect } = useContext(SideEffectContext); + // This hooks returns `rect`. const [rect, setRect] = useState(null); @@ -302,9 +305,9 @@ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | n useEffect(() => { if (currentNode !== null) { // When the DOM node is received, immedaiately calculate its DOM Rect and return that - setRect(currentNode.getBoundingClientRect()); + setRect(getBoundingClientRect(currentNode)); } - }, [currentNode]); + }, [currentNode, getBoundingClientRect]); /** * When scroll events occur, recalculate the DOMRect. DOMRect represents the position of an element relative to the viewport, so that may change during scroll (depending on the layout.) @@ -322,7 +325,7 @@ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | n const currentY = window.scrollY; if (currentNode !== null && (previousX !== currentX || previousY !== currentY)) { - setRect(currentNode.getBoundingClientRect()); + setRect(getBoundingClientRect(currentNode)); } previousX = currentX; @@ -334,13 +337,13 @@ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | n return () => { window.removeEventListener('scroll', handleScroll); }; - }, [currentNode, requestAnimationFrame]); + }, [currentNode, requestAnimationFrame, getBoundingClientRect]); useEffect(() => { if (currentNode !== null) { const resizeObserver = new ResizeObserver((entries) => { if (currentNode !== null && currentNode === entries[0].target) { - setRect(currentNode.getBoundingClientRect()); + setRect(getBoundingClientRect(currentNode)); } }); resizeObserver.observe(currentNode); @@ -348,6 +351,6 @@ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | n resizeObserver.disconnect(); }; } - }, [ResizeObserver, currentNode]); + }, [ResizeObserver, currentNode, getBoundingClientRect]); return [rect, ref]; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index c89740f667b29..4c3be81a4992a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -378,7 +378,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli ruleNote, }: UpdateTimeline): (() => void) => () => { dispatch( - sourcererActions.setSelectedIndexPatterns({ + sourcererActions.initTimelineIndexPatterns({ id: SourcererScopeName.timeline, selectedPatterns: timeline.indexNames, eventType: timeline.eventType, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 081e804cf7356..1875e0cd83cf8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -183,12 +183,7 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => search: rule.id, searchFields: ['alertId'], }); - return transformValidateBulkError( - rule.id, - rule, - ruleActions, - ruleStatuses.saved_objects[0] - ); + return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 8828bbe6c9826..ddc2ade9b5ac9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -50,7 +50,6 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => if (!siemClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } - const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( @@ -192,12 +191,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => search: rule.id, searchFields: ['alertId'], }); - return transformValidateBulkError( - rule.id, - rule, - ruleActions, - ruleStatuses.saved_objects[0] - ); + return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 06ec22b2f61b4..6bdbfedf625dd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -9,13 +9,13 @@ import { transformValidateFindAlerts, transformValidateBulkError, } from './validate'; -import { getResult } from '../__mocks__/request_responses'; import { FindResult } from '../../../../../../alerts/server'; import { BulkError } from '../utils'; -import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; +import { getResult, getFindResultStatus } from '../__mocks__/request_responses'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; -export const ruleOutput: RulesSchema = { +export const ruleOutput = (): RulesSchema => ({ actions: [], author: ['Elastic'], created_at: '2019-12-13T16:40:33.400Z', @@ -80,14 +80,14 @@ export const ruleOutput: RulesSchema = { note: '# Investigative notes', timeline_title: 'some-timeline-title', timeline_id: 'some-timeline-id', -}; +}); describe('validate', () => { describe('transformValidate', () => { test('it should do a validation correctly of a partial alert', () => { const ruleAlert = getResult(); const [validated, errors] = transformValidate(ruleAlert); - expect(validated).toEqual(ruleOutput); + expect(validated).toEqual(ruleOutput()); expect(errors).toEqual(null); }); @@ -103,14 +103,35 @@ describe('validate', () => { describe('transformValidateFindAlerts', () => { test('it should do a validation correctly of a find alert', () => { - const findResult: FindResult = { data: [getResult()], page: 1, perPage: 0, total: 0 }; + const findResult: FindResult = { + data: [getResult()], + page: 1, + perPage: 0, + total: 0, + }; const [validated, errors] = transformValidateFindAlerts(findResult, []); - expect(validated).toEqual({ data: [ruleOutput], page: 1, perPage: 0, total: 0 }); + const expected: { + page: number; + perPage: number; + total: number; + data: Array>; + } | null = { + data: [ruleOutput()], + page: 1, + perPage: 0, + total: 0, + }; + expect(validated).toEqual(expected); expect(errors).toEqual(null); }); test('it should do an in-validation correctly of a partial alert', () => { - const findResult: FindResult = { data: [getResult()], page: 1, perPage: 0, total: 0 }; + const findResult: FindResult = { + data: [getResult()], + page: 1, + perPage: 0, + total: 0, + }; // @ts-expect-error delete findResult.page; const [validated, errors] = transformValidateFindAlerts(findResult, []); @@ -123,7 +144,7 @@ describe('validate', () => { test('it should do a validation correctly of a rule id', () => { const ruleAlert = getResult(); const validatedOrError = transformValidateBulkError('rule-1', ruleAlert); - expect(validatedOrError).toEqual(ruleOutput); + expect(validatedOrError).toEqual(ruleOutput()); }); test('it should do an in-validation correctly of a rule id', () => { @@ -140,5 +161,34 @@ describe('validate', () => { }; expect(validatedOrError).toEqual(expected); }); + + test('it should do a validation correctly of a rule id with ruleStatus passed in', () => { + const ruleStatus = getFindResultStatus(); + const ruleAlert = getResult(); + const validatedOrError = transformValidateBulkError('rule-1', ruleAlert, null, ruleStatus); + const expected: RulesSchema = { + ...ruleOutput(), + status: 'succeeded', + status_date: '2020-02-18T15:26:49.783Z', + last_success_at: '2020-02-18T15:26:49.783Z', + last_success_message: 'succeeded', + }; + expect(validatedOrError).toEqual(expected); + }); + + test('it should return error object if "alert" is not expected alert type', () => { + const ruleAlert = getResult(); + // @ts-expect-error + delete ruleAlert.alertTypeId; + const validatedOrError = transformValidateBulkError('rule-1', ruleAlert); + const expected: BulkError = { + error: { + message: 'Internal error transforming', + status_code: 500, + }, + rule_id: 'rule-1', + }; + expect(validatedOrError).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index 983382b28ab38..27100eaebea15 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -22,6 +22,7 @@ import { isAlertType, IRuleSavedAttributesSavedObjectAttributes, isRuleStatusFindType, + IRuleStatusSOAttributes, } from '../../rules/types'; import { createBulkErrorObject, BulkError } from '../utils'; import { transformFindAlerts, transform, transformAlertToRule } from './utils'; @@ -74,7 +75,7 @@ export const transformValidateBulkError = ( ruleId: string, alert: PartialAlert, ruleActions?: RuleActions | null, - ruleStatus?: unknown + ruleStatus?: SavedObjectsFindResponse ): RulesSchema | BulkError => { if (isAlertType(alert)) { if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_403_response_to_a_post.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_403_response_to_a_post.json index 9139ca82cc7d8..44deb00140654 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_403_response_to_a_post.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_403_response_to_a_post.json @@ -20,9 +20,9 @@ "rule_id": "a87a4e42-1d82-44bd-b0bf-d9b7f91fb89e", "severity": "medium", "tags": [ - "APM", - "Elastic" + "Elastic", + "APM" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_405_response_method_not_allowed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_405_response_method_not_allowed.json index 2eb7d711e5fb8..5ff5cb39a6b21 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_405_response_method_not_allowed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_405_response_method_not_allowed.json @@ -20,9 +20,9 @@ "rule_id": "75ee75d8-c180-481c-ba88-ee50129a6aef", "severity": "medium", "tags": [ - "APM", - "Elastic" + "Elastic", + "APM" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_null_user_agent.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_null_user_agent.json index e78395be8fb1b..75bbfe477732b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_null_user_agent.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_null_user_agent.json @@ -38,9 +38,9 @@ "rule_id": "43303fd4-4839-4e48-b2b2-803ab060758d", "severity": "medium", "tags": [ - "APM", - "Elastic" + "Elastic", + "APM" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_sqlmap_user_agent.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_sqlmap_user_agent.json index aaaab6b5c6031..10185c28a8e52 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_sqlmap_user_agent.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/apm_sqlmap_user_agent.json @@ -20,9 +20,9 @@ "rule_id": "d49cc73f-7a16-4def-89ce-9fc7127d7820", "severity": "medium", "tags": [ - "APM", - "Elastic" + "Elastic", + "APM" ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_network_connection_from_windows_binary.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_network_connection_from_windows_binary.json deleted file mode 100644 index 5652f025952d7..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_network_connection_from_windows_binary.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "author": [ - "Elastic" - ], - "description": "Identifies network activity from unexpected system applications. This may indicate adversarial activity as these applications are often leveraged by adversaries to execute code and evade detection.", - "from": "now-9m", - "index": [ - "logs-endpoint.events.*", - "winlogbeat-*" - ], - "language": "eql", - "license": "Elastic License", - "name": "Unusual Network Activity from a Windows System Binary", - "query": "sequence by process.entity_id with maxspan=5m\n [process where event.type in (\"start\", \"process_started\") and\n\n /* known applocker bypasses */\n process.name in (\"bginfo.exe\",\n \"cdb.exe\",\n \"control.exe\",\n \"cmstp.exe\",\n \"csi.exe\",\n \"dnx.exe\",\n \"fsi.exe\",\n \"ieexec.exe\",\n \"iexpress.exe\",\n \"installutil.exe\",\n \"Microsoft.Workflow.Compiler.exe\",\n \"MSBuild.exe\",\n \"msdt.exe\",\n \"mshta.exe\",\n \"msiexec.exe\",\n \"msxsl.exe\",\n \"odbcconf.exe\",\n \"rcsi.exe\",\n \"regsvr32.exe\",\n \"xwizard.exe\")]\n [network where event.type == \"connection\" and\n process.name in (\"bginfo.exe\",\n \"cdb.exe\",\n \"control.exe\",\n \"cmstp.exe\",\n \"csi.exe\",\n \"dnx.exe\",\n \"fsi.exe\",\n \"ieexec.exe\",\n \"iexpress.exe\",\n \"installutil.exe\",\n \"Microsoft.Workflow.Compiler.exe\",\n \"MSBuild.exe\",\n \"msdt.exe\",\n \"mshta.exe\",\n \"msiexec.exe\",\n \"msxsl.exe\",\n \"odbcconf.exe\",\n \"rcsi.exe\",\n \"regsvr32.exe\",\n \"xwizard.exe\")]\n", - "risk_score": 21, - "rule_id": "1fe3b299-fbb5-4657-a937-1d746f2c711a", - "severity": "medium", - "tags": [ - "Elastic", - "Windows" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ - { - "id": "T1127", - "name": "Trusted Developer Utilities Proxy Execution", - "reference": "https://attack.mitre.org/techniques/T1127/" - } - ] - } - ], - "type": "eql", - "version": 1 -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json index 3dc084a3af54b..7c98f47fd05e2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_cloudtrail_logging_created.json @@ -25,11 +25,12 @@ "rule_id": "594e0cbf-86cc-45aa-9ff7-ff27db27d3ed", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Logging", - "Continuous Monitoring" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_subscription_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_subscription_creation.json index 720c6f71dafdd..6b90ec776926c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_subscription_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_subscription_creation.json @@ -22,10 +22,11 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", - "Logging" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_topic_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_topic_creation.json index 93695334faae2..e53c36b236639 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_topic_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_gcp_pub_sub_topic_creation.json @@ -22,10 +22,11 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", - "Logging" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_update_event_hub_auth_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_update_event_hub_auth_rule.json index cddc98ba2e6d7..d65a0bcdbc6d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_update_event_hub_auth_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/collection_update_event_hub_auth_rule.json @@ -22,11 +22,12 @@ "rule_id": "b6dce542-2b75-4ffb-b7d6-38787298ba9d", "severity": "medium", "tags": [ - "Azure", "Elastic", - "SecOps", + "Cloud", + "Azure", "Continuous Monitoring", - "Logging" + "SecOps", + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json index f32877da78d99..e88297aa2c813 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_certutil_network_connection.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_beacon.json index 7ebc13ac8079b..27ad410df1fa2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_beacon.json @@ -23,7 +23,9 @@ "severity": "high", "tags": [ "Elastic", - "Network" + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json index 79ec202c41ffb..3df567b09055a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json @@ -23,7 +23,9 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -43,5 +45,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_download_rar_powershell_from_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_download_rar_powershell_from_internet.json index 7b739f005a0cb..92411011ba66e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_download_rar_powershell_from_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_download_rar_powershell_from_internet.json @@ -23,7 +23,9 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_fin7_c2_behavior.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_fin7_c2_behavior.json index 04d68aff0da1c..1ea40aad7861a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_fin7_c2_behavior.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_fin7_c2_behavior.json @@ -22,7 +22,9 @@ "severity": "high", "tags": [ "Elastic", - "Network" + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json index 9a009ffd3fd21..c73fdf1bded9d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -54,5 +58,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_halfbaked_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_halfbaked_beacon.json index 7dacb9afcbd60..19c2832b4b82e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_halfbaked_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_halfbaked_beacon.json @@ -23,7 +23,9 @@ "severity": "high", "tags": [ "Elastic", - "Network" + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json index 0e35d4b1c5ca0..f1901fa70def2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json index e42bf4029eb01..5afdd1f629ae4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_nat_traversal_port_activity.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -39,5 +43,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json index 1cdfd44eb2adf..edd913da4d2b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_26_activity.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -23,7 +24,10 @@ "severity": "low", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json index 319f95ed88e08..0c35bd5e23ed5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -39,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_pptp_point_to_point_tunneling_protocol_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_pptp_point_to_point_tunneling_protocol_activity.json index bd478f2b23fc0..c706a5b7248c8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_pptp_point_to_point_tunneling_protocol_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_pptp_point_to_point_tunneling_protocol_activity.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -39,5 +43,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json index ee02505300611..8535a9591b88f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -39,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json index 87544647b17e1..4a3fd026f54a7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -69,5 +73,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json index d5b21dfe2db18..596c4bbac57ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License", "name": "Remote File Download via Desktopimgdownldr Utility", - "query": "event.category:process and event.type:(start or process_started) and (process.name:desktopimgdownldr.exe or process.pe.original_file_name:desktopimgdownldr.exe) and process.args:/lockscreenurl\\:http*", + "query": "event.category:process and event.type:(start or process_started) and (process.name:desktopimgdownldr.exe or process.pe.original_file_name:desktopimgdownldr.exe or winlog.event_data.OriginalFileName:desktopimgdownldr.exe) and process.args:/lockscreenurl\\:http*", "references": [ "https://labs.sentinelone.com/living-off-windows-land-a-new-native-file-downldr/" ], @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json index aeadc849eac17..9eef2fbbc62a6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json @@ -12,7 +12,7 @@ "license": "Elastic License", "name": "Remote File Download via MpCmdRun", "note": "### Investigating Remote File Download via MpCmdRun\nVerify details such as the parent process, URL reputation, and downloaded file details. Additionally, `MpCmdRun` logs this information in the Appdata Temp folder in `MpCmdRun.log`.", - "query": "event.category:process and event.type:(start or process_started) and (process.name:MpCmdRun.exe or process.pe.original_file_name:MpCmdRun.exe) and process.args:((\"-DownloadFile\" or \"-downloadfile\") and \"-url\" and \"-path\")", + "query": "event.category:process and event.type:(start or process_started) and (process.name:MpCmdRun.exe or process.pe.original_file_name:MpCmdRun.exe or winlog.event_data.OriginalFileName:MpCmdRun.exe) and process.args:((\"-DownloadFile\" or \"-downloadfile\") and \"-url\" and \"-path\")", "references": [ "https://twitter.com/mohammadaskar2/status/1301263551638761477", "https://www.bleepingcomputer.com/news/microsoft/microsoft-defender-can-ironically-be-used-to-download-malware/" @@ -22,7 +22,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json index 3a082c29a4cf1..f041255374f12 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -54,5 +58,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json index 95ac4d8836800..7e4f3907fc31e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -39,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json index fe5608459ffce..08ab14aeb5c7c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -69,5 +73,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json index 9ecfe39a79303..4bc48ebe0c316 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -39,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_teamviewer_remote_file_copy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_teamviewer_remote_file_copy.json index 793ff4ebda72f..c597f6b2718b9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_teamviewer_remote_file_copy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_teamviewer_remote_file_copy.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json index 4455d8adfdf83..9007db322ae58 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_telnet_port_activity.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json index b278c36d01c1b..e82106a87bc2e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { @@ -54,5 +58,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json index 97d2b940a6949..9321d2a2103de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json index 97757af22be0c..38f38e9762645 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Command and Control" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json index 118f8f6b2ad4a..fb8256bf2509c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json @@ -21,10 +21,11 @@ "severity": "high", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json index 5aae95476e9da..d8d5b5305aaaa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Identity", "Okta", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json index e350c3697f685..b7e89a9120bbb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_aws_iam_assume_role_brute_force.json @@ -21,11 +21,12 @@ "rule_id": "ea248a02-bc47-4043-8e94-2885b19b2636", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_compress_credentials_keychains.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_compress_credentials_keychains.json similarity index 95% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_compress_credentials_keychains.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_compress_credentials_keychains.json index bf2a52066ae1c..c13ac69e50987 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_compress_credentials_keychains.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_compress_credentials_keychains.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "MacOS" + "Host", + "macOS", + "Threat Detection", + "Credential Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json index a67fa01ab371a..0761ba515d9b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Credential Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_domain_backup_dpapi_private_keys.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_domain_backup_dpapi_private_keys.json index dc4f5e11754d3..eefd6ee9e601b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_domain_backup_dpapi_private_keys.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_domain_backup_dpapi_private_keys.json @@ -22,7 +22,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Credential Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_gcp_iam_service_account_key_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_gcp_iam_service_account_key_deletion.json index 63d5081869f1b..5db891caa2857 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_gcp_iam_service_account_key_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_gcp_iam_service_account_key_deletion.json @@ -23,6 +23,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_gcp_key_created_for_service_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_gcp_key_created_for_service_account.json index c1ae7f5fc1953..a6d45b7465771 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_gcp_key_created_for_service_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_gcp_key_created_for_service_account.json @@ -23,6 +23,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json index 7c5aa9bc7f3a8..8244cb755787f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iam_user_addition_to_group.json @@ -24,11 +24,12 @@ "rule_id": "333de828-8190-4cf5-8d7c-7575846f6fe0", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json index dd7bc43c58382..6a182617945f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json @@ -12,7 +12,7 @@ "license": "Elastic License", "max_signals": 33, "name": "Microsoft IIS Service Account Password Dumped", - "query": "event.category:process AND event.type:(start OR process_started) AND (process.name:appcmd.exe OR process.pe.original_file_name:appcmd.exe) AND process.args:(/[lL][iI][sS][tT]/ AND /\\/[tT][eE][xX][tT]\\:[pP][aA][sS][sS][wW][oO][rR][dD]/)", + "query": "event.category:process AND event.type:(start OR process_started) AND (process.name:appcmd.exe OR process.pe.original_file_name:appcmd.exe or winlog.event_data.OriginalFileName:appcmd.exe) AND process.args:(/[lL][iI][sS][tT]/ AND /\\/[tT][eE][xX][tT]\\:[pP][aA][sS][sS][wW][oO][rR][dD]/)", "references": [ "https://blog.netspi.com/decrypting-iis-passwords-to-break-out-of-the-dmz-part-1/" ], @@ -21,7 +21,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Credential Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json index 2735fcbbd6130..f750a0f5594b4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json @@ -12,7 +12,7 @@ "license": "Elastic License", "max_signals": 33, "name": "Microsoft IIS Connection Strings Decryption", - "query": "event.category:process and event.type:(start or process_started) and (process.name:aspnet_regiis.exe or process.pe.original_file_name:aspnet_regiis.exe) and process.args:(connectionStrings and \"-pdf\")", + "query": "event.category:process and event.type:(start or process_started) and (process.name:aspnet_regiis.exe or process.pe.original_file_name:aspnet_regiis.exe or winlog.event_data.OriginalFileName:aspnet_regiis.exe) and process.args:(connectionStrings and \"-pdf\")", "references": [ "https://blog.netspi.com/decrypting-iis-passwords-to-break-out-of-the-dmz-part-1/", "https://symantec-enterprise-blogs.security.com/blogs/threat-intelligence/greenbug-espionage-telco-south-asia" @@ -22,7 +22,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Credential Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberosdump_kcc.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberosdump_kcc.json index 4713d09f8adec..dda2f67c11663 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberosdump_kcc.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberosdump_kcc.json @@ -21,7 +21,10 @@ "severity": "high", "tags": [ "Elastic", - "MacOS" + "Host", + "macOS", + "Threat Detection", + "Credential Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_key_vault_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_key_vault_modified.json index a45591c73dcb3..33df4e5930066 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_key_vault_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_key_vault_modified.json @@ -24,9 +24,10 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "Azure", - "SecOps", "Continuous Monitoring", + "SecOps", "Data Protection" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mimikatz_memssp_default_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mimikatz_memssp_default_logs.json index fa1f99eef7f00..ae9b63371a452 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mimikatz_memssp_default_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_mimikatz_memssp_default_logs.json @@ -17,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Credential Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json index c36f878792ccf..9e10dd6dae522 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json @@ -24,10 +24,11 @@ "severity": "medium", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_root_console_failure_brute_force.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_root_console_failure_brute_force.json similarity index 96% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_root_console_failure_brute_force.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_root_console_failure_brute_force.json index 5f7781be82efd..f65a7b11d11ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_root_console_failure_brute_force.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_root_console_failure_brute_force.json @@ -23,11 +23,12 @@ "rule_id": "4d50a94f-2844-43fa-8395-6afbd5e1c5ef", "severity": "high", "tags": [ + "Elastic", + "Cloud", "AWS", "Continuous Monitoring", - "Elastic", - "Identity and Access", - "SecOps" + "SecOps", + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json index 879e93750df9c..a33593f701ba7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_secretsmanager_getsecretvalue.json @@ -26,11 +26,12 @@ "rule_id": "a00681e3-9ed6-447c-ab2c-be648821c622", "severity": "high", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Data Protection", - "Continuous Monitoring" + "Data Protection" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_storage_account_key_regenerated.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_storage_account_key_regenerated.json index 2a3dc85294a9d..62e1aab700680 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_storage_account_key_regenerated.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_storage_account_key_regenerated.json @@ -22,10 +22,11 @@ "rule_id": "1e0b832e-957e-43ae-b319-db82d228c908", "severity": "low", "tags": [ - "Azure", "Elastic", - "SecOps", + "Cloud", + "Azure", "Continuous Monitoring", + "SecOps", "Identity and Access" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json index 306a38f5d2a28..67ab62e8d0ee5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_tcpdump_activity.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Credential Access" ], "threat": [ { @@ -55,5 +58,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json index c80f24a21d958..a1ff4bfc890a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_adding_the_hidden_file_attribute_with_via_attribexe.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -52,5 +55,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json index 4d4f10bbaa599..b17e4979a885c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_iptables_or_firewall.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json index 3c34b04a77a50..960000c91e4fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_to_disable_syslog_service.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_diagnostic_settings_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_diagnostic_settings_deletion.json index 49d98813dc040..7721790b5cf97 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_diagnostic_settings_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_diagnostic_settings_deletion.json @@ -23,9 +23,10 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "Azure", - "SecOps", "Continuous Monitoring", + "SecOps", "Monitoring" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json index 3cdfac92572b1..140e1ccd8e890 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -55,5 +58,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json index 2d26d867b8718..fa322fca5db8a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -55,5 +58,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json index 60ce575148f4c..11d57b855f974 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json index 64261af2a3105..169f429a6dd26 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_deleted.json @@ -25,11 +25,12 @@ "rule_id": "7024e2a0-315d-4334-bb1a-441c593e16ab", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Logging", - "Continuous Monitoring" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json index 090073698026d..cbd040a7f7a30 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudtrail_logging_suspended.json @@ -25,11 +25,12 @@ "rule_id": "1aa8fa52-44a7-4dae-b058-f3333b91c8d7", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Logging", - "Continuous Monitoring" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json index aeaf0a4168814..e18deb65c497b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cloudwatch_alarm_deletion.json @@ -25,11 +25,12 @@ "rule_id": "f772ec8a-e182-483c-91d2-72058f76a44c", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_code_injection_conhost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_code_injection_conhost.json index 63c7ea12b3b6b..ed1b8978c6329 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_code_injection_conhost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_code_injection_conhost.json @@ -21,7 +21,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json index 268f52a8efd5e..b7d9321814fd7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_config_service_rule_deletion.json @@ -25,11 +25,12 @@ "rule_id": "7024e2a0-315d-4334-bb1a-552d604f27bc", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json index b926937450f5a..b28572deaf204 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_configuration_recorder_stopped.json @@ -25,11 +25,12 @@ "rule_id": "fbd44836-0d69-4004-a0b4-03c20370c435", "severity": "high", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json index 2abad3c255f15..3beb71763f1ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_cve_2020_0601.json @@ -15,7 +15,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -35,5 +38,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json index 50213b9f1a42c..5fde3c462eded 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_delete_volume_usn_journal_with_fsutil.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json index 026735f413eab..554ccc6972e5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_backup_catalogs_with_wbadmin.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json index 85d8bdcb2582f..eef37499c8eb5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deletion_of_bash_command_line_history.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json index d107c0b262091..35476a76fd4b5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_selinux_attempt.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json index 6fbf9ca800f79..a69fde9f6a5cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_disable_windows_firewall_rules_with_netsh.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_dotnet_compiler_parent_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json similarity index 93% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_dotnet_compiler_parent_process.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json index 0a675bd7aab74..08cbb33710b26 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_dotnet_compiler_parent_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json index 9f3d4e6b5e379..d36294684698e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_flow_log_deletion.json @@ -25,11 +25,12 @@ "rule_id": "9395fd2c-9947-4472-86ef-4aceb2f7e872", "severity": "high", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Logging", - "Continuous Monitoring" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json index 6ecc9ad3d558d..b6ac9be800807 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_ec2_network_acl_deletion.json @@ -27,11 +27,12 @@ "rule_id": "8623535c-1e17-44e1-aa97-7a0699c3037d", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Network", - "Continuous Monitoring" + "Network Security" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json index 0d47aab2c64bd..f00ac3f90527b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_encoding_or_decoding_files_via_certutil.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_event_hub_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_event_hub_deletion.json index 29df07cced4d7..d09edf473c939 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_event_hub_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_event_hub_deletion.json @@ -24,11 +24,12 @@ "rule_id": "e0f36de1-0342-453d-95a9-a068b257b053", "severity": "medium", "tags": [ - "Azure", "Elastic", - "SecOps", + "Cloud", + "Azure", "Continuous Monitoring", - "Logging" + "SecOps", + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json index a987c00b392ec..c41b3aad2f42c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_office_app.json @@ -23,7 +23,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json index 0537f27bad463..a8e16d8bda238 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_script.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json index 11fdd128475dc..60c9a317bbb6c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_by_system_process.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json index a90e5ebc57800..5daab573db5bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json index 4f353a6ff9e6f..09247d2f21323 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_unusal_process.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -43,5 +46,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json index 69d334cf13fdb..7d9f190ba7be2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json @@ -11,13 +11,16 @@ "language": "kuery", "license": "Elastic License", "name": "Potential DLL SideLoading via Trusted Microsoft Programs", - "query": "event.category:process and event.type:(start or process_started) and process.pe.original_file_name:(WinWord.exe or EXPLORER.EXE or w3wp.exe or DISM.EXE) and not (process.name:(winword.exe or WINWORD.EXE or explorer.exe or w3wp.exe or Dism.exe) or process.executable:(\"C:\\Windows\\explorer.exe\" or C\\:\\\\Program?Files\\\\Microsoft?Office\\\\root\\\\Office*\\\\WINWORD.EXE or C\\:\\\\Program?Files?\\(x86\\)\\\\Microsoft?Office\\\\root\\\\Office*\\\\WINWORD.EXE or \"C:\\Windows\\System32\\Dism.exe\" or \"C:\\Windows\\SysWOW64\\Dism.exe\" or \"C:\\Windows\\System32\\inetsrv\\w3wp.exe\"))", + "query": "event.category:process and event.type:(start or process_started) and (process.pe.original_file_name:(WinWord.exe or EXPLORER.EXE or w3wp.exe or DISM.EXE) or winlog.event_data.OriginalFileName:(WinWord.exe or EXPLORER.EXE or w3wp.exe or DISM.EXE)) and not (process.name:(winword.exe or WINWORD.EXE or explorer.exe or w3wp.exe or Dism.exe) or process.executable:(\"C:\\Windows\\explorer.exe\" or C\\:\\\\Program?Files\\\\Microsoft?Office\\\\root\\\\Office*\\\\WINWORD.EXE or C\\:\\\\Program?Files?\\(x86\\)\\\\Microsoft?Office\\\\root\\\\Office*\\\\WINWORD.EXE or \"C:\\Windows\\System32\\Dism.exe\" or \"C:\\Windows\\SysWOW64\\Dism.exe\" or \"C:\\Windows\\System32\\inetsrv\\w3wp.exe\"))", "risk_score": 73, "rule_id": "1160dcdb-0a0a-4a79-91d8-9b84616edebd", "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_via_trusted_developer_utilities.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_via_trusted_developer_utilities.json index a1d14155cc3b3..7963c03699f78 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_via_trusted_developer_utilities.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_via_trusted_developer_utilities.json @@ -7,7 +7,8 @@ "These programs may be used by Windows developers but use by non-engineers is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -18,7 +19,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json index 5b02f63a1c7f7..dc73b7bc1eb76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_deletion_via_shred.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json index 8ee2d4fda7bf8..b97e5c1d6a182 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_file_mod_writable_dir.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -40,5 +43,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_firewall_policy_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_firewall_policy_deletion.json index 759fc9d5ecb1f..69a123ba678fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_firewall_policy_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_firewall_policy_deletion.json @@ -22,11 +22,12 @@ "rule_id": "e02bd3ea-72c6-4181-ac2b-0f83d17ad969", "severity": "low", "tags": [ - "Azure", "Elastic", - "SecOps", + "Cloud", + "Azure", "Continuous Monitoring", - "Network" + "SecOps", + "Network Security" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_created.json index b80a5f0e17949..dc08dace20bfc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_created.json @@ -22,6 +22,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_deleted.json index 64c8d01df47e9..7ee5af109f37b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_deleted.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_modified.json index b2c0e259b45e0..b4107fb9f08fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_firewall_rule_modified.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_bucket_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_bucket_deletion.json index 62447b789d632..079a87b5c615b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_bucket_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_bucket_deletion.json @@ -23,10 +23,11 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", - "Logging" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_sink_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_sink_deletion.json index 0fc83070ffbb7..8466b618fab98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_sink_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_logging_sink_deletion.json @@ -22,10 +22,11 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", - "Logging" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_subscription_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_subscription_deletion.json index 2ae47140b66a5..5b87b8722595c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_subscription_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_subscription_deletion.json @@ -22,10 +22,11 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", - "Logging" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_topic_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_topic_deletion.json index f276af3e21862..5a681a35006a7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_topic_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_pub_sub_topic_deletion.json @@ -22,10 +22,11 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", - "Logging" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_configuration_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_configuration_modified.json index 3b18732137c32..5992beef9873e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_configuration_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_configuration_modified.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_permissions_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_permissions_modified.json index ad6beb9383eea..0687bb1e5178a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_permissions_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_gcp_storage_bucket_permissions_modified.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json index 3910b8e4039ad..50ee5a902b144 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_guardduty_detector_deletion.json @@ -25,11 +25,12 @@ "rule_id": "523116c0-d89d-4d7c-82c2-39e6845a78ef", "severity": "high", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json index f5345b2276e8a..6d3d6f456da4c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -55,5 +58,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json index e66968a50709e..c21c15909d82a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hidden_file_dir_tmp.json @@ -21,7 +21,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -56,5 +59,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json index 507260f04d016..7d75f50856125 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json @@ -12,13 +12,16 @@ "license": "Elastic License", "max_signals": 33, "name": "IIS HTTP Logging Disabled", - "query": "event.category:process and event.type:(start or process_started) and (process.name:appcmd.exe or process.pe.original_file_name:appcmd.exe) and process.args:/dontLog\\:\\\"True\\\" and not process.parent.name:iissetup.exe", + "query": "event.category:process and event.type:(start or process_started) and (process.name:appcmd.exe or process.pe.original_file_name:appcmd.exe or winlog.event_data.OriginalFileName:appcmd.exe) and process.args:/dontLog\\:\\\"True\\\" and not process.parent.name:iissetup.exe", "risk_score": 73, "rule_id": "ebf1adea-ccf2-4943-8b96-7ab11ca173a5", "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json index 9abce01769e92..7978ef9004271 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_injection_msbuild.json @@ -18,7 +18,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -53,5 +56,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_installutil_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_installutil_beacon.json similarity index 67% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_installutil_beacon.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_installutil_beacon.json index 7437bf27141ec..231ed1b36dc0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_installutil_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_installutil_beacon.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "InstallUtil Process Making Network Connections", - "query": "/* this can be done without a sequence however, this does include more info on the process */\n\nsequence by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.name == \"installutil.exe\"]\n [network where event.type == \"connection\" and process.name == \"installutil.exe\" and network.direction == \"outgoing\"]\n", + "query": "/* the benefit of doing this as an eql sequence vs kql is this will limit to alerting only on the first network connection */\n\nsequence by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.name : \"installutil.exe\"]\n [network where event.type == \"connection\" and process.name : \"installutil.exe\" and network.direction == \"outgoing\"]\n", "risk_score": 21, "rule_id": "a13167f1-eec2-4015-9631-1fee60406dcf", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json index ad751a1031437..5c38974b46525 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_kernel_module_removal.json @@ -23,7 +23,10 @@ "severity": "high", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -58,5 +61,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json index 8b7ef47443e2f..163c7e834ba34 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json index cc964bfdd3e92..be83f8c41a2ea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json @@ -11,13 +11,16 @@ "language": "lucene", "license": "Elastic License", "name": "Renamed AutoIt Scripts Interpreter", - "query": "event.category:process AND event.type:(start OR process_started) AND process.pe.original_file_name:/[aA][uU][tT][oO][iI][tT]\\d\\.[eE][xX][eE]/ AND NOT process.name:/[aA][uU][tT][oO][iI][tT]\\d{1,3}\\.[eE][xX][eE]/", + "query": "event.category:process AND event.type:(start OR process_started) AND (process.pe.original_file_name:/[aA][uU][tT][oO][iI][tT]\\d\\.[eE][xX][eE]/ OR winlog.event_data.OriginalFileName:/[aA][uU][tT][oO][iI][tT]\\d\\.[eE][xX][eE]/) AND NOT process.name:/[aA][uU][tT][oO][iI][tT]\\d{1,3}\\.[eE][xX][eE]/", "risk_score": 47, "rule_id": "2e1e835d-01e5-48ca-b9fc-7a61f7f11902", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_suspicious_werfault_childproc.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_suspicious_werfault_childproc.json index 3000e7ac86daa..df0ecf1d534a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_suspicious_werfault_childproc.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_suspicious_werfault_childproc.json @@ -24,7 +24,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json index db421146085ff..9f5615d466374 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json @@ -24,7 +24,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json index 5b5f69a0aef74..3d87720818ff5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_misc_lolbin_connecting_to_the_internet.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -52,5 +55,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json index 6025fc5ca6452..84ccc52249622 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_modification_of_boot_config.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_msbuild_beacon_sequence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msbuild_beacon_sequence.json similarity index 79% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_msbuild_beacon_sequence.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msbuild_beacon_sequence.json index 59295c3735a3a..0740e26d3bba6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_msbuild_beacon_sequence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msbuild_beacon_sequence.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "MsBuild Network Connection Sequence", - "query": "sequence by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.name == \"MSBuild.exe\"]\n [network where process.name == \"MSBuild.exe\" and\n not (destination.address == \"127.0.0.1\" and source.address == \"127.0.0.1\")]\n", + "query": "sequence by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.name : \"MSBuild.exe\"]\n [network where process.name : \"MSBuild.exe\" and\n not (destination.ip == \"127.0.0.1\" and source.ip == \"127.0.0.1\")]\n", "risk_score": 21, "rule_id": "9dc6ed5d-62a9-4feb-a903-fafa1d33b8e9", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_mshta_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mshta_beacon.json similarity index 69% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_mshta_beacon.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mshta_beacon.json index 105f536628777..fd19942a33d48 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_mshta_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mshta_beacon.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "Mshta Making Network Connections", - "query": "sequence by process.entity_id with maxspan=2h\n [process where event.type in (\"start\", \"process_started\") and process.name == \"mshta.exe\" and\n process.parent.name != \"Microsoft.ConfigurationManagement.exe\" and\n process.parent.executable not in (\"C:\\\\Amazon\\\\Amazon Assistant\\\\amazonAssistantService.exe\",\n \"C:\\\\TeamViewer\\\\TeamViewer.exe\") and\n process.args != \"ADSelfService_Enroll.hta\"]\n [network where process.name == \"mshta.exe\"]\n", + "query": "sequence by process.entity_id with maxspan=2h\n [process where event.type in (\"start\", \"process_started\") and process.name : \"mshta.exe\" and\n not process.parent.name : \"Microsoft.ConfigurationManagement.exe\" and\n not (process.parent.executable : \"C:\\\\Amazon\\\\Amazon Assistant\\\\amazonAssistantService.exe\" or\n process.parent.executable : \"C:\\\\TeamViewer\\\\TeamViewer.exe\") and\n not process.args : \"ADSelfService_Enroll.hta\"]\n [network where process.name : \"mshta.exe\"]\n", "risk_score": 21, "rule_id": "c2d90150-0133-451c-a783-533e736c12d7", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_msxsl_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_beacon.json similarity index 79% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_msxsl_beacon.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_beacon.json index 27704b3e182ed..c188387304214 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_msxsl_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_msxsl_beacon.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "MsXsl Making Network Connections", - "query": "sequence by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.name == \"msxsl.exe\"]\n [network where event.type == \"connection\" and process.name == \"msxsl.exe\" and network.direction == \"outgoing\"]\n", + "query": "sequence by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.name : \"msxsl.exe\"]\n [network where event.type == \"connection\" and process.name : \"msxsl.exe\" and network.direction == \"outgoing\"]\n", "risk_score": 21, "rule_id": "870d1753-1078-403e-92d4-735f142edcca", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_connection_from_windows_binary.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_connection_from_windows_binary.json new file mode 100644 index 0000000000000..4e1d0cad0b5da --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_connection_from_windows_binary.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies network activity from unexpected system applications. This may indicate adversarial activity as these applications are often leveraged by adversaries to execute code and evade detection.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Unusual Network Activity from a Windows System Binary", + "query": "sequence by process.entity_id with maxspan=5m\n [process where event.type in (\"start\", \"process_started\") and\n\n /* known applocker bypasses */\n (process.name : \"bginfo.exe\" or\n process.name : \"cdb.exe\" or\n process.name : \"control.exe\" or\n process.name : \"cmstp.exe\" or\n process.name : \"csi.exe\" or\n process.name : \"dnx.exe\" or\n process.name : \"fsi.exe\" or\n process.name : \"ieexec.exe\" or\n process.name : \"iexpress.exe\" or\n process.name : \"installutil.exe\" or\n process.name : \"Microsoft.Workflow.Compiler.exe\" or\n process.name : \"MSBuild.exe\" or\n process.name : \"msdt.exe\" or\n process.name : \"mshta.exe\" or\n process.name : \"msiexec.exe\" or\n process.name : \"msxsl.exe\" or\n process.name : \"odbcconf.exe\" or\n process.name : \"rcsi.exe\" or\n process.name : \"regsvr32.exe\" or\n process.name : \"xwizard.exe\")]\n [network where event.type == \"connection\" and\n (process.name : \"bginfo.exe\" or\n process.name : \"cdb.exe\" or\n process.name : \"control.exe\" or\n process.name : \"cmstp.exe\" or\n process.name : \"csi.exe\" or\n process.name : \"dnx.exe\" or\n process.name : \"fsi.exe\" or\n process.name : \"ieexec.exe\" or\n process.name : \"iexpress.exe\" or\n process.name : \"installutil.exe\" or\n process.name : \"Microsoft.Workflow.Compiler.exe\" or\n process.name : \"MSBuild.exe\" or\n process.name : \"msdt.exe\" or\n process.name : \"mshta.exe\" or\n process.name : \"msiexec.exe\" or\n process.name : \"msxsl.exe\" or\n process.name : \"odbcconf.exe\" or\n process.name : \"rcsi.exe\" or\n process.name : \"regsvr32.exe\" or\n process.name : \"xwizard.exe\")]\n", + "risk_score": 21, + "rule_id": "1fe3b299-fbb5-4657-a937-1d746f2c711a", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1127", + "name": "Trusted Developer Utilities Proxy Execution", + "reference": "https://attack.mitre.org/techniques/T1127/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_watcher_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_watcher_deletion.json index 09bbba5a049e4..0e6d9172eb2c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_watcher_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_network_watcher_deletion.json @@ -23,10 +23,11 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "Azure", - "SecOps", "Continuous Monitoring", - "Network" + "SecOps", + "Network Security" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_reg_beacon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_reg_beacon.json similarity index 66% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_reg_beacon.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_reg_beacon.json index 332c719eaa41d..aa4f9985f6e2c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_reg_beacon.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_reg_beacon.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "Registration Tool Making Network Connections", - "query": "sequence by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and\n process.name in (\"regasm.exe\", \"regsvcs.exe\", \"regsvr32.exe\")]\n [network where event.type == \"connection\" and process.name in (\"regasm.exe\", \"regsvcs.exe\", \"regsvr32.exe\")]\nuntil\n [process where event.type == \"end\" and process.name in (\"regasm.exe\", \"regsvcs.exe\", \"regsvr32.exe\")]\n", + "query": "sequence by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and\n (process.name : \"RegAsm.exe\" or process.name : \"regsvcs.exe\" or process.name : \"regsvr32.exe\")]\n [network where event.type == \"connection\" and\n (process.name : \"RegAsm.exe\" or process.name : \"regsvcs.exe\" or process.name : \"regsvr32.exe\")]\nuntil\n [process where event.type == \"end\" and\n (process.name : \"RegAsm.exe\" or process.name : \"regsvcs.exe\" or process.name : \"regsvr32.exe\")]\n", "risk_score": 21, "rule_id": "6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/evasion_rundll32_no_arguments.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json similarity index 61% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/evasion_rundll32_no_arguments.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json index 06fad7e0f630b..2950b792219b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/evasion_rundll32_no_arguments.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "Unusual Child Processes of RunDLL32", - "query": "sequence with maxspan=1h\n [process where event.type in (\"start\", \"process_started\") and\n (process.name == \"rundll32.exe\" or process.pe.original_file_name == \"rundll32.exe\") and\n\n /* zero arguments excluding the binary itself (and accounting for when the binary may not be logged in args) */\n ((process.args == \"rundll32.exe\" and process.args_count == 1) or\n (process.args != \"rundll32.exe\" and process.args_count == 0))\n\n ] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and\n (process.name == \"rundll32.exe\" or process.pe.original_file_name == \"rundll32.exe\")\n ] by process.parent.entity_id\n", + "query": "sequence with maxspan=1h\n [process where event.type in (\"start\", \"process_started\") and\n /* uncomment once in winlogbeat */\n (process.name : \"rundll32.exe\" /* or process.pe.original_file_name == \"RUNDLL32.EXE\" */ ) and\n process.args_count < 2\n ] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and\n /* uncomment once in winlogbeat */\n (process.name : \"rundll32.exe\" /* or process.pe.original_file_name == \"RUNDLL32.EXE\" */ )\n ] by process.parent.entity_id\n", "risk_score": 21, "rule_id": "f036953a-4615-4707-a1ca-dc53bf69dcd5", "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_rundll32_sequence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_sequence.json similarity index 67% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_rundll32_sequence.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_sequence.json index 6f465325039a6..c022d0a603858 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_rundll32_sequence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_sequence.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "Unusual Network Connection Sequence via RunDLL32", - "query": "sequence by process.entity_id with maxspan=2h\n [process where event.type in (\"start\", \"process_started\") and\n (process.name == \"rundll32.exe\" or process.pe.original_file_name == \"rundll32.exe\") and\n\n /* zero arguments excluding the binary itself (and accounting for when the binary may not be logged in args) */\n ((process.args == \"rundll32.exe\" and process.args_count == 1) or\n (process.args != \"rundll32.exe\" and process.args_count == 0))]\n\n [network where event.type == \"connection\" and\n (process.name == \"rundll32.exe\" or process.pe.original_file_name == \"rundll32.exe\")]\n", + "query": "sequence by process.entity_id with maxspan=2h\n [process where event.type in (\"start\", \"process_started\") and\n /* uncomment once in winlogbeat */\n (process.name : \"rundll32.exe\" /* or process.pe.original_file_name == \"RUNDLL32.EXE\" */ ) and\n process.args_count < 2]\n [network where event.type == \"connection\" and\n /* uncomment once in winlogbeat */\n (process.name : \"rundll32.exe\" /* or process.pe.original_file_name == \"RUNDLL32.EXE\" */ )]\n", "risk_score": 21, "rule_id": "2b347f66-6739-4ae3-bd94-195036dde8b3", "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json index adbe310b784e5..6cfdf40781397 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_s3_bucket_configuration_deletion.json @@ -28,11 +28,12 @@ "rule_id": "227dc608-e558-43d9-b521-150772250bae", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Asset Visibility", - "Continuous Monitoring" + "Asset Visibility" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json index ec3030d44ff29..6fea9a3c78945 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json @@ -18,7 +18,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_managedcode_host_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_managedcode_host_process.json index 1bb3f26c0298f..85eac81808a71 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_managedcode_host_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_managedcode_host_process.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_suspicious_okta_user_password_reset_or_unlock_attempts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json similarity index 99% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_suspicious_okta_user_password_reset_or_unlock_attempts.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json index 5f115416fa032..fedeaca68ab64 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_suspicious_okta_user_password_reset_or_unlock_attempts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json @@ -25,6 +25,7 @@ "severity": "medium", "tags": [ "Elastic", + "Identity", "Okta", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/evasion_suspicious_scrobj_load.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_scrobj_load.json similarity index 51% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/evasion_suspicious_scrobj_load.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_scrobj_load.json index 7880b86533b53..16364f590cd0e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/evasion_suspicious_scrobj_load.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_scrobj_load.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "Windows Suspicious Script Object Execution", - "query": "sequence by process.entity_id with maxspan=2m\n [process where event.type in (\"start\", \"process_started\") and\n /* process.code_signature.* fields need to be populated for 7.10 */\n process.code_signature.subject_name == \"Microsoft Corporation\" and process.code_signature.trusted == true and\n process.name not in (\"cscript.exe\",\n \"iexplore.exe\",\n \"MicrosoftEdge.exe\",\n \"msiexec.exe\",\n \"smartscreen.exe\",\n \"taskhostw.exe\",\n \"w3wp.exe\",\n \"wscript.exe\")]\n [library where event.type == \"start\" and file.name == \"scrobj.dll\"]\n", + "query": "/* add winlogbeat-* when process.code_signature.* fields are populated */\n\nsequence by process.entity_id with maxspan=2m\n [process where event.type in (\"start\", \"process_started\") and\n /* uncomment once in winlogbeat */\n /* process.code_signature.subject_name == \"Microsoft Corporation\" and process.code_signature.trusted == true and */\n not (process.name : \"cscript.exe\" or\n process.name : \"iexplore.exe\" or\n process.name : \"MicrosoftEdge.exe\" or\n process.name : \"msiexec.exe\" or\n process.name : \"smartscreen.exe\" or\n process.name : \"taskhostw.exe\" or\n process.name : \"w3wp.exe\" or\n process.name : \"wscript.exe\")]\n [library where event.type == \"start\" and file.name : \"scrobj.dll\"]\n", "risk_score": 21, "rule_id": "4ed678a9-3a4f-41fb-9fea-f85a6e0a0dff", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/evasion_suspicious_wmi_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_wmi_script.json similarity index 59% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/evasion_suspicious_wmi_script.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_wmi_script.json index 943471f5801c2..e922416264359 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/evasion_suspicious_wmi_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_wmi_script.json @@ -5,19 +5,21 @@ "description": "Identifies WMIC whitelisting bypass techniques by alerting on suspicious execution of scripts. When WMIC loads scripting libraries it may be indicative of a whitelist bypass.", "from": "now-9m", "index": [ - "logs-endpoint.events.*", - "winlogbeat-*" + "logs-endpoint.events.*" ], "language": "eql", "license": "Elastic License", "name": "Suspicious WMIC XSL Script Execution", - "query": "/* lots of wildcards in the args\n need to verify args cleanup is accurate\n*/\n\nsequence by process.entity_id with maxspan=2m\n[process where event.type in (\"start\", \"process_started\") and\n (process.name == \"wmic.exe\" or process.pe.original_file_name == \"wmic.exe\") and\n wildcard(process.args, \"format*:*\", \"/format*:*\", \"*-format*:*\") and\n not process.args in (\"/format:table\", \"/format:table\") or wildcard(process.args, \"format*:*\")]\n[library where event.type == \"start\" and file.name in (\"jscript.dll\", \"vbscript.dll\")]\n", + "query": "sequence by process.entity_id with maxspan=2m\n[process where event.type in (\"start\", \"process_started\") and\n (process.name : \"WMIC.exe\" or process.pe.original_file_name == \"wmic.exe\") and\n wildcard(process.args, \"format*:*\", \"/format*:*\", \"*-format*:*\") and\n not wildcard(process.command_line, \"* /format:table *\")]\n[library where event.type == \"start\" and file.name in (\"jscript.dll\", \"vbscript.dll\")]\n", "risk_score": 21, "rule_id": "7f370d54-c0eb-4270-ac5a-9a6020585dc6", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json index 7b08f5a565424..f3c20e5251184 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_system_critical_proc_abnormal_file_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_system_critical_proc_abnormal_file_activity.json index 6fea3a75c8e62..31702332d7cd4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_system_critical_proc_abnormal_file_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_system_critical_proc_abnormal_file_activity.json @@ -17,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_system_vp_child_program.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_system_vp_child_program.json index 4efec948f49a7..65439d6d653b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_system_vp_child_program.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_system_vp_child_program.json @@ -17,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_via_filter_manager.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_via_filter_manager.json index 210e9c778afef..3092ea653aa4b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_via_filter_manager.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_via_filter_manager.json @@ -4,7 +4,8 @@ ], "description": "The Filter Manager Control Program (fltMC.exe) binary may be abused by adversaries to unload a filter driver and evade defenses.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -15,7 +16,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json index 8a504281b03f7..493ee919000dc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_vssadmin.json @@ -17,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json index 2ae938bb34104..49a68f4bfcf66 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_volume_shadow_copy_deletion_via_wmic.json @@ -17,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json index 32101029fb107..86ed9e2f9c042 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_acl_deletion.json @@ -25,11 +25,12 @@ "rule_id": "91d04cd4-47a9-4334-ab14-084abe274d49", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Network", - "Continuous Monitoring" + "Network Security" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json index a08c05d0d6ca7..302e89d416f4b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_waf_rule_or_rule_group_deletion.json @@ -25,11 +25,12 @@ "rule_id": "5beaebc1-cc13-4bfc-9949-776f9e0dc318", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Network", - "Continuous Monitoring" + "Network Security" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_blob_container_access_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_blob_container_access_mod.json index 7e601c9928d08..16db02338de55 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_blob_container_access_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_blob_container_access_mod.json @@ -23,9 +23,10 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "Azure", - "SecOps", "Continuous Monitoring", + "SecOps", "Asset Visibility" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json index af9c4b5409964..f2681f5501af0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_kernel_module_enumeration.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Discovery" ], "threat": [ { @@ -40,5 +43,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json index 96c300cfde016..682434f28f745 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_command_system_account.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Discovery" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_post_exploitation_public_ip_reconnaissance.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_post_exploitation_public_ip_reconnaissance.json index 952d70ee3589a..36e95cf4ade41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_post_exploitation_public_ip_reconnaissance.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_post_exploitation_public_ip_reconnaissance.json @@ -24,8 +24,8 @@ "tags": [ "Elastic", "Network", - "Threat Detection, Preventing and Hunting", - "Post-Execution" + "Threat Detection", + "Discovery" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_process_discovery_via_tasklist_command.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_process_discovery_via_tasklist_command.json index c2d95de4129f9..59ddc00d6525d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_process_discovery_via_tasklist_command.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_process_discovery_via_tasklist_command.json @@ -7,7 +7,8 @@ "Administrators may use the tasklist command to display a list of currently running processes. By itself, it does not indicate malicious activity. After obtaining a foothold, it's possible adversaries may use discovery commands like tasklist to get information about running processes." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -18,7 +19,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Discovery" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json index d913a92e2ee0e..66b744667f568 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_virtual_machine_fingerprinting.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Discovery" ], "threat": [ { @@ -40,5 +43,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_command_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_command_activity.json index cb330879be9b3..2e577e44c960e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_command_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_command_activity.json @@ -7,7 +7,8 @@ "Some normal use of this program, at varying levels of frequency, may originate from scripts, automation tools and frameworks. Usage by non-engineers and ordinary users is unusual." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -18,7 +19,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Discovery" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json index a8b34362d9579..ddf309b377609 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_whoami_commmand.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Discovery" ], "threat": [ { @@ -40,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json index f3acc5d3a2b5e..d6e7419391f9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json @@ -60,7 +60,7 @@ ], "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "timestamp_override": "event.ingested", "type": "query", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_adversary_behavior_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_adversary_behavior_detected.json index b3bac305bc1f1..8084067b3a6d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_adversary_behavior_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_adversary_behavior_detected.json @@ -17,7 +17,7 @@ "severity": "medium", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_dumping_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_dumping_detected.json index 2f91c1fe813f9..9c28d065b322d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_dumping_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_dumping_detected.json @@ -17,7 +17,7 @@ "severity": "high", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_dumping_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_dumping_prevented.json index 75488c2d3a5ed..352712e38f42d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_dumping_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_dumping_prevented.json @@ -17,7 +17,7 @@ "severity": "medium", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_manipulation_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_manipulation_detected.json index adc29d9106774..259bcd51aeb3e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_manipulation_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_manipulation_detected.json @@ -17,7 +17,7 @@ "severity": "high", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_manipulation_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_manipulation_prevented.json index 99def69978a48..19348062b10f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_manipulation_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_cred_manipulation_prevented.json @@ -17,7 +17,7 @@ "severity": "medium", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_exploit_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_exploit_detected.json index 80eb3ce637f30..2fd3aaa0d8a57 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_exploit_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_exploit_detected.json @@ -17,7 +17,7 @@ "severity": "high", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_exploit_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_exploit_prevented.json index 50444904654de..8f90e1162546b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_exploit_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_exploit_prevented.json @@ -17,7 +17,7 @@ "severity": "medium", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_malware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_malware_detected.json index bb2ddf92a83e7..3d740f8b7064f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_malware_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_malware_detected.json @@ -17,7 +17,7 @@ "severity": "critical", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_malware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_malware_prevented.json index fae8a3a0ab5a6..33195c7fcbecc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_malware_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_malware_prevented.json @@ -17,7 +17,7 @@ "severity": "high", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_permission_theft_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_permission_theft_detected.json index 821c3b0d8a63b..fac13a6d358dd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_permission_theft_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_permission_theft_detected.json @@ -17,7 +17,7 @@ "severity": "high", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_permission_theft_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_permission_theft_prevented.json index e38afe19e7d38..a2d8700076c23 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_permission_theft_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_permission_theft_prevented.json @@ -17,7 +17,7 @@ "severity": "medium", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_process_injection_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_process_injection_detected.json index 52eb3c2d96bf7..ef4f29067b0c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_process_injection_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_process_injection_detected.json @@ -17,7 +17,7 @@ "severity": "high", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_process_injection_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_process_injection_prevented.json index 76aff15e1588c..b22751e35c053 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_process_injection_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_process_injection_prevented.json @@ -17,7 +17,7 @@ "severity": "medium", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_ransomware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_ransomware_detected.json index 29efdd910904d..3b973f42bbca5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_ransomware_detected.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_ransomware_detected.json @@ -17,7 +17,7 @@ "severity": "critical", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_ransomware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_ransomware_prevented.json index c603e503c5dad..b6458b73e8015 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_ransomware_prevented.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endpoint_ransomware_prevented.json @@ -17,7 +17,7 @@ "severity": "high", "tags": [ "Elastic", - "Endpoint" + "Endpoint Security" ], "type": "query", "version": 4 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/escalation_uac_sdclt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/escalation_uac_sdclt.json deleted file mode 100644 index 843ba3401b4e4..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/escalation_uac_sdclt.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "author": [ - "Elastic" - ], - "description": "Identifies User Account Control (UAC) bypass via sdclt.exe. Attackers bypass UAC to stealthily execute code with elevated permissions.", - "from": "now-9m", - "index": [ - "logs-endpoint.events.*", - "winlogbeat-*" - ], - "language": "eql", - "license": "Elastic License", - "name": "Bypass UAC via Sdclt", - "query": "sequence with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and process.name == \"sdclt.exe\" and\n /* process.code_signature.* fields need to be populated for 7.10 */\n process.code_signature.subject_name == \"Microsoft Corporation\" and process.code_signature.trusted == true and\n process.args == \"/kickoffelev\"\n ] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.parent.name == \"sdclt.exe\" and\n process.executable not in (\"C:\\\\Windows\\\\System32\\\\sdclt.exe\",\n \"C:\\\\Windows\\\\System32\\\\control.exe\",\n \"C:\\\\Windows\\\\SysWOW64\\\\sdclt.exe\",\n \"C:\\\\Windows\\\\SysWOW64\\\\control.exe\")\n ] by process.parent.entity_id\n", - "risk_score": 21, - "rule_id": "9b54e002-034a-47ac-9307-ad12c03fa900", - "severity": "high", - "tags": [ - "Elastic", - "Windows" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0004", - "name": "Privilege Escalation", - "reference": "https://attack.mitre.org/tactics/TA0004/" - }, - "technique": [ - { - "id": "T1088", - "name": "Bypass User Account Control", - "reference": "https://attack.mitre.org/techniques/T1088/" - } - ] - } - ], - "type": "eql", - "version": 1 -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json index e9989fe50019e..d73b1a4cab008 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_prompt_connecting_to_the_internet.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json index bb252b1416832..220a7f94dce9a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json index aeae5518fece1..d6b1e67b11982 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_svchost.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_unusual_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_unusual_process.json index 577bee1ffe6de..566a5963c122a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_unusual_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_unusual_process.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_virtual_machine.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_virtual_machine.json index 5e7852e1c1b13..31c4d488c6960 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_virtual_machine.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_virtual_machine.json @@ -24,11 +24,12 @@ "rule_id": "60884af6-f553-4a6c-af13-300047455491", "severity": "medium", "tags": [ - "Azure", "Elastic", - "SecOps", + "Cloud", + "Azure", "Continuous Monitoring", - "Logging" + "SecOps", + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json index 963c6b2e53ed6..d33f2287c7d8b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_html_help_executable_program_connecting_to_the_internet.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -52,5 +55,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json index 7b20cefdc67f0..693ca83e387b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_local_service_commands.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_ms_office_written_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_ms_office_written_file.json index 35d7a7c969ee7..2db46080a4e75 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_ms_office_written_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_ms_office_written_file.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "Execution of File Written or Modified by Microsoft Office", - "query": "sequence with maxspan=2h\n [file where event.type != \"delete\" and file.extension == \"exe\" and\n process.name in (\"winword.exe\",\n \"excel.exe\",\n \"outlook.exe\",\n \"powerpnt.exe\",\n \"eqnedt32.exe\",\n \"fltldr.exe\",\n \"mspub.exe\",\n \"msaccess.exe\")\n ] by host.id, file.path\n [process where event.type in (\"start\", \"process_started\")] by host.id, process.executable\n", + "query": "sequence with maxspan=2h\n [file where event.type != \"deletion\" and file.extension : \"exe\" and\n (process.name : \"WINWORD.EXE\" or\n process.name : \"EXCEL.EXE\" or\n process.name : \"OUTLOOK.EXE\" or\n process.name : \"POWERPNT.EXE\" or\n process.name : \"eqnedt32.exe\" or\n process.name : \"fltldr.exe\" or\n process.name : \"MSPUB.EXE\" or\n process.name : \"MSACCESS.EXE\")\n ] by host.id, file.path\n [process where event.type in (\"start\", \"process_started\")] by host.id, process.executable\n", "risk_score": 21, "rule_id": "0d8ad79f-9025-45d8-80c1-4f0cd3c5e8e5", "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json index fcbbfbdb3d686..7fd2933fe46f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msbuild_making_network_connections.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json index 7af823070889f..9b863c2ed5ee4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_mshta_making_network_connections.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -40,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json index 1dc75575636fb..17987218af0ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_msxsl_network.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pdf_written_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pdf_written_file.json index 3963b3d594902..a807052cf7b0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pdf_written_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_pdf_written_file.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "Execution of File Written or Modified by PDF Reader", - "query": "sequence with maxspan=2h\n [file where event.type != \"delete\" and file.extension == \"exe\" and\n process.name in (\"acrord32.exe\", \"rdrcef.exe\", \"foxitphantomPDF.exe\", \"foxitreader.exe\") and\n file.name not in (\"foxitphantomPDF.exe\",\n \"FoxitPhantomPDFUpdater.exe\",\n \"foxitreader.exe\",\n \"FoxitReaderUpdater.exe\",\n \"acrord32.exe\",\n \"rdrcef.exe\")\n ] by host.id, file.path\n [process where event.type in (\"start\", \"process_started\")] by host.id, process.executable\n", + "query": "sequence with maxspan=2h\n [file where event.type != \"deletion\" and file.extension : \"exe\" and\n (process.name : \"AcroRd32.exe\" or\n process.name : \"rdrcef.exe\" or\n process.name : \"FoxitPhantomPDF.exe\" or\n process.name : \"FoxitReader.exe\") and\n not (file.name : \"FoxitPhantomPDF.exe\" or\n file.name : \"FoxitPhantomPDFUpdater.exe\" or\n file.name : \"FoxitReader.exe\" or\n file.name : \"FoxitReaderUpdater.exe\" or\n file.name : \"AcroRd32.exe\" or\n file.name : \"rdrcef.exe\")\n ] by host.id, file.path\n [process where event.type in (\"start\", \"process_started\")] by host.id, process.executable\n", "risk_score": 21, "rule_id": "1defdd62-cd8d-426e-a246-81a37751bb2b", "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json index 4502f42bbb4c4..ee6cee941bef2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_perl_tty_shell.json @@ -17,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json index f647d8d00e084..8505a837ad591 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_psexec_lateral_movement_command.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -55,5 +58,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json index 0e8b5f0218d00..151c3fbb8df46 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_python_tty_shell.json @@ -17,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json index 899bb1c20e711..bf1a30f11137e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_register_server_program_connecting_to_the_internet.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json index 6d7f11f01fae0..843cf322e5849 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_script_executing_powershell.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json index 005a0c38c8a8b..90c60ceea37ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json index 74e21c7d17479..d5ee8fa818367 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_outlook_child_process.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json index adf1a76bfb901..bf37f5a803e58 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_pdf_reader.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_psexesvc.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json similarity index 76% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_psexesvc.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json index 51396fb5995f6..205b5148f2fb4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_psexesvc.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json @@ -11,13 +11,16 @@ "language": "kuery", "license": "Elastic License", "name": "Suspicious Process Execution via Renamed PsExec Executable", - "query": "event.category:process and event.type:(start or process_started) and process.pe.original_file_name:(psexesvc.exe or PSEXESVC.exe) and process.parent.name:services.exe and not process.name:(psexesvc.exe or PSEXESVC.exe)", + "query": "event.category:process and event.type:(start or process_started) and (process.pe.original_file_name:(psexesvc.exe or PSEXESVC.exe) or winlog.event_data.OriginalFileName:(psexesvc.exe or PSEXESVC.exe)) and process.parent.name:services.exe and not process.name:(psexesvc.exe or PSEXESVC.exe)", "risk_score": 47, "rule_id": "e2f9fdf5-8076-45ad-9427-41e0e03dc9c2", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_children.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_children.json index 96305b2197bfc..52e67b0c7bcff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_children.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_children.json @@ -7,7 +7,8 @@ "Werfault.exe will legitimately spawn when dns.exe crashes, but the DNS service is very stable and so this is a low occurring event. Denial of Service (DoS) attempts by intentionally crashing the service will also cause werfault.exe to spawn." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -24,7 +25,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_file_writes.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_file_writes.json index c175ecbfa78b6..229fc28beee9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_file_writes.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_file_writes.json @@ -4,7 +4,8 @@ ], "description": "Identifies an unexpected file being modified by dns.exe, the process responsible for Windows DNS Server services, which may indicate activity related to remote code execution or other forms of exploitation.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -20,7 +21,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json index 1104159350655..2763f69e1f8e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json index 774e8e9189ced..877c489b0d187 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_process_network_connection.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_compiled_html_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_compiled_html_file.json index fe3e110830420..276e5c18335f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_compiled_html_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_compiled_html_file.json @@ -7,7 +7,8 @@ "The HTML Help executable program (hh.exe) runs whenever a user clicks a compiled help (.chm) file or menu item that opens the help file inside the Help Viewer. This is not always malicious, but adversaries may abuse this technology to conceal malicious code." ], "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -18,7 +19,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_hidden_shell_conhost.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_hidden_shell_conhost.json index 7fbf962469f71..b6f6502e603aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_hidden_shell_conhost.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_hidden_shell_conhost.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json index d9dcbfe25a4c2..0a21599c31a4a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_net_com_assemblies.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { @@ -52,5 +55,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json index 081ebcb518999..13493a90e3e50 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json @@ -24,11 +24,12 @@ "rule_id": "37b211e8-4e2f-440f-86d8-06cc8f158cfa", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Logging", - "Continuous Monitoring" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_xp_cmdshell_mssql_stored_procedure.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_xp_cmdshell_mssql_stored_procedure.json index 8769e641fad90..91097b2050d17 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_xp_cmdshell_mssql_stored_procedure.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_xp_cmdshell_mssql_stored_procedure.json @@ -17,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_wpad_exploitation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_wpad_exploitation.json index 03c4482b60340..a09bce5119ecf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_wpad_exploitation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_wpad_exploitation.json @@ -5,19 +5,21 @@ "description": "Identifies probable exploitation of the Web Proxy Auto-Discovery Protocol (WPAD) service. Attackers who have access to the local network or upstream DNS traffic can inject malicious JavaScript to the WPAD service which can lead to a full system compromise.", "from": "now-9m", "index": [ - "logs-endpoint.events.*", - "winlogbeat-*" + "logs-endpoint.events.*" ], "language": "eql", "license": "Elastic License", "name": "WPAD Service Exploit", - "query": "/* preference would be to use user.sid rather than domain+name, once it is available in ECS + datasources */\n\nsequence with maxspan=5s\n [process where event.type in (\"start\", \"process_started\") and process.name == \"svchost.exe\" and\n user.domain == \"NT AUTHORITY\" and user.name == \"LOCAL SERVICE\"] by process.entity_id\n [network where network.protocol == \"dns\" and process.name == \"svchost.exe\" and\n dns.question.name == \"wpad\" and process.name == \"svchost.exe\"] by process.entity_id\n [network where event.type == \"connection\" and process.name == \"svchost.exe\"\n and network.direction == \"outgoing\" and destination.port == 80] by process.entity_id\n [library where event.type == \"start\" and process.name == \"svchost.exe\" and\n file.name == \"jscript.dll\" and process.name == \"svchost.exe\"] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and\n process.parent.name == \"svchost.exe\"] by process.parent.entity_id\n", + "query": "/* preference would be to use user.sid rather than domain+name, once it is available in ECS + datasources */\n\nsequence with maxspan=5s\n [process where event.type in (\"start\", \"process_started\") and process.name : \"svchost.exe\" and\n user.domain == \"NT AUTHORITY\" and user.name == \"LOCAL SERVICE\"] by process.entity_id\n [network where network.protocol == \"dns\" and process.name : \"svchost.exe\" and\n dns.question.name : \"wpad\" and process.name : \"svchost.exe\"] by process.entity_id\n [network where event.type == \"connection\" and process.name : \"svchost.exe\"\n and network.direction == \"outgoing\" and destination.port == 80] by process.entity_id\n [library where event.type == \"start\" and process.name : \"svchost.exe\" and\n file.name : \"jscript.dll\" and process.name : \"svchost.exe\"] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and\n process.parent.name : \"svchost.exe\"] by process.parent.entity_id\n", "risk_score": 21, "rule_id": "ec328da1-d5df-482b-866c-4a435692b1f3", "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Execution" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json index fc18a516be0f4..abf64d55ab99a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_ec2_snapshot_change_activity.json @@ -25,11 +25,12 @@ "rule_id": "98fd7407-0bd5-5817-cda0-3fcc33113a56", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Asset Visibility", - "Continuous Monitoring" + "Asset Visibility" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_gcp_logging_sink_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_gcp_logging_sink_modification.json index 4e8954c3441cd..5e3cc2da2f871 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_gcp_logging_sink_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_gcp_logging_sink_modification.json @@ -22,10 +22,11 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", - "Logging" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json index 6bc14f4e5af8a..a8a86348a1019 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/external_alerts.json @@ -54,9 +54,14 @@ } ], "tags": [ - "Elastic" + "Elastic", + "Network", + "Windows", + "APM", + "macOS", + "Linux" ], "timestamp_override": "event.ingested", "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json index f9d71a2e1cbff..f2ad30fa26020 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_automation_runbook_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_automation_runbook_deleted.json index 662709774f5ba..f474357cc6e2c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_automation_runbook_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_azure_automation_runbook_deleted.json @@ -23,6 +23,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json index c648ae1ea4b5b..d7f4c2b19bc0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudtrail_logging_updated.json @@ -25,11 +25,12 @@ "rule_id": "3e002465-876f-4f04-b016-84ef48ce7e5d", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Logging", - "Continuous Monitoring" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json index f7e9077c14314..ae978f6564d67 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_group_deletion.json @@ -25,11 +25,12 @@ "rule_id": "68a7a5a5-a2fc-4a76-ba9f-26849de881b4", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Logging", - "Continuous Monitoring" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json index b50efb21e42f0..7b985fdb6f693 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_cloudwatch_log_stream_deletion.json @@ -25,11 +25,12 @@ "rule_id": "d624f0ae-3dd1-4856-9aad-ccfe4d4bfa17", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Logging", - "Continuous Monitoring" + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json index 370a65c31e7c1..c60619e894717 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_ec2_disable_ebs_encryption.json @@ -26,11 +26,12 @@ "rule_id": "bb9b13b2-1700-48a8-a750-b43b0a72ab69", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Data Protection", - "Continuous Monitoring" + "Data Protection" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_iam_role_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_iam_role_deletion.json index 2c67be7408d1d..09a9996680155 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_iam_role_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_iam_role_deletion.json @@ -22,6 +22,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_deleted.json index 2aa702e5ca4d1..9d34f31c1700d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_deleted.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_disabled.json index 9b5188f43633d..606ebd1e6128e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_disabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_service_account_disabled.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_storage_bucket_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_storage_bucket_deleted.json index 6adad4b687de7..859c59ff8a325 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_storage_bucket_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_storage_bucket_deleted.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_network_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_network_deleted.json index c5dc9f25f893f..7f702f11a9515 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_network_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_network_deleted.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_created.json index 5e8fea09befc4..1da90189f96b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_created.json @@ -23,6 +23,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_deleted.json index 8482e0efbb036..c379f07f021a6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_deleted.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_gcp_virtual_private_cloud_route_deleted.json @@ -23,6 +23,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_hosts_file_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_hosts_file_modified.json index bf04626dee277..5d7e0bec4332c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_hosts_file_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_hosts_file_modified.json @@ -22,9 +22,12 @@ "severity": "medium", "tags": [ "Elastic", + "Host", "Linux", "Windows", - "macOS" + "macOS", + "Threat Detection", + "Impact" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json index ca5cfd4ae596b..83e184d9bf805 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_deactivate_mfa_device.json @@ -25,11 +25,12 @@ "rule_id": "d8fc1cca-93ed-43c1-bbb6-c0dd3eff2958", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json index 00a10772d4d9a..6212aa70747a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_iam_group_deletion.json @@ -25,11 +25,12 @@ "rule_id": "867616ec-41e5-4edc-ada2-ab13ab45de8a", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json index 9bc44bf4e6da9..d1852478c666f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json @@ -21,10 +21,11 @@ "severity": "medium", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json index 829c244dd45c0..116483173564e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_cluster_deletion.json @@ -27,11 +27,12 @@ "rule_id": "9055ece6-2689-4224-a0e0-b04881e1f8ad", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Asset Visibility", - "Continuous Monitoring" + "Asset Visibility" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json index 68459d0e777b9..3227649149380 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_rds_instance_cluster_stoppage.json @@ -27,11 +27,12 @@ "rule_id": "ecf2b32c-e221-4bd4-aa3b-c7d59b3bc01d", "severity": "medium", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Asset Visibility", - "Continuous Monitoring" + "Asset Visibility" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_resource_group_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_resource_group_deletion.json index a0c56c19b964e..8086c09e4b174 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_resource_group_deletion.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_resource_group_deletion.json @@ -23,10 +23,11 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "Azure", - "SecOps", "Continuous Monitoring", - "Logging" + "SecOps", + "Log Auditing" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts index 6e376930617de..5fec97e83bad4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -221,14 +221,14 @@ import rule209 from './credential_access_okta_brute_force_or_password_spraying.j import rule210 from './execution_unusual_dns_service_children.json'; import rule211 from './execution_unusual_dns_service_file_writes.json'; import rule212 from './lateral_movement_dns_server_overflow.json'; -import rule213 from './initial_access_root_console_failure_brute_force.json'; +import rule213 from './credential_access_root_console_failure_brute_force.json'; import rule214 from './initial_access_unsecure_elasticsearch_node.json'; import rule215 from './credential_access_domain_backup_dpapi_private_keys.json'; -import rule216 from './lateral_movement_gpo_schtask_service_creation.json'; -import rule217 from './credential_access_kerberosdump_kcc.json'; -import rule218 from './defense_evasion_execution_suspicious_psexesvc.json'; -import rule219 from './execution_via_xp_cmdshell_mssql_stored_procedure.json'; -import rule220 from './exfiltration_compress_credentials_keychains.json'; +import rule216 from './persistence_gpo_schtask_service_creation.json'; +import rule217 from './credential_access_compress_credentials_keychains.json'; +import rule218 from './credential_access_kerberosdump_kcc.json'; +import rule219 from './execution_suspicious_psexesvc.json'; +import rule220 from './execution_via_xp_cmdshell_mssql_stored_procedure.json'; import rule221 from './privilege_escalation_printspooler_service_suspicious_file.json'; import rule222 from './privilege_escalation_printspooler_suspicious_spl_file.json'; import rule223 from './defense_evasion_azure_diagnostic_settings_deletion.json'; @@ -250,16 +250,16 @@ import rule238 from './persistence_azure_automation_webhook_created.json'; import rule239 from './privilege_escalation_uac_bypass_diskcleanup_hijack.json'; import rule240 from './credential_access_attempts_to_brute_force_okta_user_account.json'; import rule241 from './credential_access_storage_account_key_regenerated.json'; -import rule242 from './credential_access_suspicious_okta_user_password_reset_or_unlock_attempts.json'; +import rule242 from './defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json'; import rule243 from './defense_evasion_system_critical_proc_abnormal_file_activity.json'; import rule244 from './defense_evasion_unusual_system_vp_child_program.json'; -import rule245 from './defense_evasion_mfa_disabled_for_azure_user.json'; -import rule246 from './discovery_blob_container_access_mod.json'; +import rule245 from './discovery_blob_container_access_mod.json'; +import rule246 from './persistence_mfa_disabled_for_azure_user.json'; import rule247 from './persistence_user_added_as_owner_for_azure_application.json'; import rule248 from './persistence_user_added_as_owner_for_azure_service_principal.json'; -import rule249 from './defense_evasion_suspicious_managedcode_host_process.json'; -import rule250 from './execution_command_shell_started_by_unusual_process.json'; -import rule251 from './execution_suspicious_dotnet_compiler_parent_process.json'; +import rule249 from './defense_evasion_dotnet_compiler_parent_process.json'; +import rule250 from './defense_evasion_suspicious_managedcode_host_process.json'; +import rule251 from './execution_command_shell_started_by_unusual_process.json'; import rule252 from './defense_evasion_masquerading_as_elastic_endpoint_process.json'; import rule253 from './defense_evasion_masquerading_suspicious_werfault_childproc.json'; import rule254 from './defense_evasion_masquerading_werfault.json'; @@ -268,28 +268,28 @@ import rule256 from './credential_access_mimikatz_memssp_default_logs.json'; import rule257 from './defense_evasion_code_injection_conhost.json'; import rule258 from './defense_evasion_network_watcher_deletion.json'; import rule259 from './initial_access_external_guest_user_invite.json'; -import rule260 from './defense_evasion_azure_conditional_access_policy_modified.json'; -import rule261 from './defense_evasion_azure_privileged_identity_management_role_modified.json'; -import rule262 from './defense_evasion_masquerading_renamed_autoit.json'; -import rule263 from './impact_azure_automation_runbook_deleted.json'; -import rule264 from './initial_access_consent_grant_attack_via_azure_registered_application.json'; -import rule265 from './c2_installutil_beacon.json'; -import rule266 from './c2_msbuild_beacon_sequence.json'; -import rule267 from './c2_mshta_beacon.json'; -import rule268 from './c2_msxsl_beacon.json'; -import rule269 from './c2_network_connection_from_windows_binary.json'; -import rule270 from './c2_reg_beacon.json'; -import rule271 from './c2_rundll32_sequence.json'; -import rule272 from './command_and_control_teamviewer_remote_file_copy.json'; -import rule273 from './escalation_uac_sdclt.json'; -import rule274 from './evasion_rundll32_no_arguments.json'; -import rule275 from './evasion_suspicious_scrobj_load.json'; -import rule276 from './evasion_suspicious_wmi_script.json'; -import rule277 from './execution_ms_office_written_file.json'; -import rule278 from './execution_pdf_written_file.json'; -import rule279 from './execution_wpad_exploitation.json'; -import rule280 from './lateral_movement_cmd_service.json'; -import rule281 from './persistence_app_compat_shim.json'; +import rule260 from './defense_evasion_masquerading_renamed_autoit.json'; +import rule261 from './impact_azure_automation_runbook_deleted.json'; +import rule262 from './initial_access_consent_grant_attack_via_azure_registered_application.json'; +import rule263 from './persistence_azure_conditional_access_policy_modified.json'; +import rule264 from './persistence_azure_privileged_identity_management_role_modified.json'; +import rule265 from './command_and_control_teamviewer_remote_file_copy.json'; +import rule266 from './defense_evasion_installutil_beacon.json'; +import rule267 from './defense_evasion_msbuild_beacon_sequence.json'; +import rule268 from './defense_evasion_mshta_beacon.json'; +import rule269 from './defense_evasion_msxsl_beacon.json'; +import rule270 from './defense_evasion_network_connection_from_windows_binary.json'; +import rule271 from './defense_evasion_reg_beacon.json'; +import rule272 from './defense_evasion_rundll32_no_arguments.json'; +import rule273 from './defense_evasion_rundll32_sequence.json'; +import rule274 from './defense_evasion_suspicious_scrobj_load.json'; +import rule275 from './defense_evasion_suspicious_wmi_script.json'; +import rule276 from './execution_ms_office_written_file.json'; +import rule277 from './execution_pdf_written_file.json'; +import rule278 from './execution_wpad_exploitation.json'; +import rule279 from './lateral_movement_cmd_service.json'; +import rule280 from './persistence_app_compat_shim.json'; +import rule281 from './privilege_escalation_uac_sdclt.json'; import rule282 from './command_and_control_remote_file_copy_desktopimgdownldr.json'; import rule283 from './command_and_control_remote_file_copy_mpcmdrun.json'; import rule284 from './defense_evasion_execution_suspicious_explorer_winword.json'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json index 8147859fa4e6f..1dab4e8df71b4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json @@ -20,6 +20,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json index 621881e264138..1d6f5b2c90f26 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_console_login_root.json @@ -24,11 +24,12 @@ "rule_id": "e2a67480-3b79-403d-96e3-fdd2992c50ef", "severity": "high", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_external_guest_user_invite.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_external_guest_user_invite.json index 392e0ec745fc2..455fc3c762978 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_external_guest_user_invite.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_external_guest_user_invite.json @@ -23,9 +23,10 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "Azure", - "SecOps", "Continuous Monitoring", + "SecOps", "Identity and Access" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_gcp_iam_custom_role_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_gcp_iam_custom_role_creation.json index 0eab41ad8c4bd..ff7ad0e8d29a2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_gcp_iam_custom_role_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_gcp_iam_custom_role_creation.json @@ -22,6 +22,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json index 2f0eed31d05be..13279ea170358 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_password_recovery.json @@ -24,11 +24,12 @@ "rule_id": "69c420e8-6c9e-4d28-86c0-8a2be2d1e78c", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json index 15c3c81a551bd..ce0f44713523f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json @@ -8,7 +8,8 @@ ], "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -19,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Initial Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json index 7c61f95f9e9f3..b8f3e01823312 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json @@ -5,7 +5,8 @@ "description": "This rule detects network events that may indicate the use of RPC traffic from the Internet. RPC is commonly used by system administrators to remotely control a system for maintenance or to use shared resources. It should almost never be directly exposed to the Internet, as it is frequently targeted and exploited by threat actors as an initial access or back-door vector.", "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -16,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Initial Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json index e8da93ed9d1c7..e8e4ea4eb3746 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json @@ -5,7 +5,8 @@ "description": "This rule detects network events that may indicate the use of RPC traffic to the Internet. RPC is commonly used by system administrators to remotely control a system for maintenance or to use shared resources. It should almost never be directly exposed to the Internet, as it is frequently targeted and exploited by threat actors as an initial access or back-door vector.", "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -16,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Initial Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json index aff8a415b7e35..fec0f308a8d27 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json @@ -5,7 +5,8 @@ "description": "This rule detects network events that may indicate the use of Windows file sharing (also called SMB or CIFS) traffic to the Internet. SMB is commonly used within networks to share files, printers, and other system resources amongst trusted systems. It should almost never be directly exposed to the Internet, as it is frequently targeted and exploited by threat actors as an initial access or back-door vector or for data exfiltration.", "index": [ "filebeat-*", - "packetbeat-*" + "packetbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -16,7 +17,10 @@ "severity": "high", "tags": [ "Elastic", - "Network" + "Host", + "Network", + "Threat Detection", + "Initial Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json index 24837084c8381..5b1946dc7c07d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json @@ -24,10 +24,11 @@ "severity": "medium", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unsecure_elasticsearch_node.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unsecure_elasticsearch_node.json index e6d718a23eb96..a577a3413e5df 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unsecure_elasticsearch_node.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_unsecure_elasticsearch_node.json @@ -23,7 +23,9 @@ "severity": "medium", "tags": [ "Elastic", - "Network" + "Network", + "Threat Detection", + "Initial Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_cmd_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_cmd_service.json index bd14db77b9fe9..f4e137663762a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_cmd_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_cmd_service.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "Service Command Lateral Movement", - "query": "/* dependent on a wildcard for remote path */\n\nsequence by process.entity_id with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and\n (process.name == \"sc.exe\" or process.pe.original_file_name == \"sc.exe\") and\n wildcard(process.args, \"\\\\\\\\*\") and wildcard(process.args, \"binPath*\", \"binpath*\") and\n process.args in (\"create\", \"config\", \"failure\", \"start\")]\n [network where event.type == \"connection\" and process.name == \"sc.exe\" and destination.address != \"127.0.0.1\"]\n", + "query": "sequence by process.entity_id with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and\n /* uncomment once in winlogbeat */\n (process.name == \"sc.exe\" /* or process.pe.original_file_name == \"sc.exe\" */ ) and\n /* case insensitive */\n wildcard(process.args, \"\\\\\\\\*\") and wildcard(process.args, \"binPath=*\", \"binpath=*\") and \n (process.args : \"create\" or\n process.args : \"config\" or\n process.args : \"failure\" or\n process.args : \"start\")]\n [network where event.type == \"connection\" and process.name : \"sc.exe\" and destination.ip != \"127.0.0.1\"]\n", "risk_score": 21, "rule_id": "d61cbcf8-1bc1-4cff-85ba-e7b21c5beedc", "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json index e4014b22a6c09..39f45a736383a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_direct_outbound_smb_connection.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dns_server_overflow.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dns_server_overflow.json index 2a86dcac12e7b..052d0de3ef79e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dns_server_overflow.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dns_server_overflow.json @@ -26,7 +26,8 @@ "tags": [ "Elastic", "Network", - "Windows" + "Threat Detection", + "Lateral Movement" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json index f1ce68abf8302..149dab112929c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_ssh_login_enabled.json @@ -21,7 +21,10 @@ "severity": "medium", "tags": [ "Elastic", - "MacOS" + "Host", + "macOS", + "Threat Detection", + "Lateral Movement" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json index e4804329c0f30..ff62251a465f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_external.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Lateral Movement" ], "threat": [ { @@ -40,5 +43,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json index 30312987d166c..bb6c03e9410e5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_telnet_network_activity_internal.json @@ -20,7 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Lateral Movement" ], "threat": [ { @@ -40,5 +43,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json index 3a5c4d9e69d49..775d201cd1808 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_hping_activity.json @@ -23,8 +23,10 @@ "severity": "high", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection" ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json index 63c82c5662df6..510eecb9d2fee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_iodine_activity.json @@ -23,8 +23,10 @@ "severity": "high", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection" ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json index 99d087fe675a6..a81567740590d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_mknod_activity.json @@ -23,7 +23,9 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection" ], "type": "query", "version": 5 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json index bce10f640691b..ebe884debc9a0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_netcat_network_connection.json @@ -25,8 +25,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection" ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json index 5d9e338425bda..76866f67d7288 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nmap_activity.json @@ -23,8 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection" ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json index bd019c9a80c4c..cc744614d68af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_nping_activity.json @@ -23,8 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection" ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json index f0bbc892d7d9c..1662d63066feb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_process_started_in_temp_directory.json @@ -20,8 +20,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection" ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json index fac03d31b57bf..07cd9e4a065b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_socat_activity.json @@ -23,8 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection" ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json index c1b782d612ccb..06be108db2f14 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/linux_strace_activity.json @@ -23,8 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection" ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_error_message_spike.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_error_message_spike.json index 0730c421cf5f2..890b777751a03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_error_message_spike.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_error_message_spike.json @@ -20,10 +20,11 @@ "rule_id": "78d3d8d9-b476-451d-a9e0-7a5addd70670", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", "ML" ], "type": "machine_learning", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_error_code.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_error_code.json index 8003cdd7504c7..62254d970ca7e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_error_code.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_error_code.json @@ -20,10 +20,11 @@ "rule_id": "19de8096-e2b0-4bd8-80c9-34a820813fff", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", "ML" ], "type": "machine_learning", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_city.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_city.json index 2c54dbd03daba..5c1189c989bac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_city.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_city.json @@ -20,10 +20,11 @@ "rule_id": "809b70d3-e2c3-455e-af1b-2626a5a1a276", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", "ML" ], "type": "machine_learning", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_country.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_country.json index 68cbf4979a933..149ebd1a84fd4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_country.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_country.json @@ -20,10 +20,11 @@ "rule_id": "dca28dee-c999-400f-b640-50a081cc0fd1", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", "ML" ], "type": "machine_learning", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_user.json index e4ec651e71934..a8c0bce169432 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_user.json @@ -20,10 +20,11 @@ "rule_id": "ac706eae-d5ec-4b14-b4fd-e8ba8086f0e1", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", "ML" ], "type": "machine_learning", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_compiler_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_compiler_activity.json index eb764c5e40817..592bd723e1a0c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_compiler_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_compiler_activity.json @@ -17,7 +17,9 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_kernel_module_arguments.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_kernel_module_arguments.json index d289e0ba6f008..52a1d6dd5c60a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_kernel_module_arguments.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_kernel_module_arguments.json @@ -20,7 +20,9 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_process.json index c1cc619164b1f..ec033a609cac1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_process.json @@ -11,13 +11,15 @@ "interval": "15m", "license": "Elastic License", "machine_learning_job_id": "linux_rare_metadata_process", - "name": "Unusual Process Calling the Metadata Service", + "name": "Unusual Linux Process Calling the Metadata Service", "risk_score": 21, "rule_id": "9d302377-d226-4e12-b54c-1906b5aec4f6", "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_user.json index 59a04dd54dd89..a4a6b38bd32b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_metadata_user.json @@ -17,7 +17,9 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_activity.json index bf86f78fe3e72..04df4adab8525 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_activity.json @@ -18,9 +18,11 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json index a588a6f5bcb0a..c5a0891a7bbc4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json @@ -20,9 +20,11 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_service.json index 5c56845024eb2..81dff605dc779 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_service.json @@ -20,9 +20,11 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_url_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_url_activity.json index 3b3f751dfc60b..5336c31d4a741 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_url_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_url_activity.json @@ -20,9 +20,11 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json index 8475410735f34..04d112c26e886 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json @@ -21,9 +21,11 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_sudo_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_sudo_activity.json index 8f03b24a6bd18..57d5260d111db 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_sudo_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_sudo_activity.json @@ -17,7 +17,9 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json index 3e4b1f15fdce4..3f889dc529317 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json @@ -21,9 +21,11 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_information_discovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_information_discovery.json index 40f117c6a5708..cdd1ee8d46653 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_information_discovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_information_discovery.json @@ -17,7 +17,9 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_network_configuration_discovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_network_configuration_discovery.json index 326024114f145..b143293ac995b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_network_configuration_discovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_network_configuration_discovery.json @@ -17,7 +17,9 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_network_connection_discovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_network_connection_discovery.json index 881a2f9fa3410..ffab1c9eb8495 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_network_connection_discovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_network_connection_discovery.json @@ -17,7 +17,9 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_process_discovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_process_discovery.json index 66859e2f9ccbf..69d2940029011 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_process_discovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_process_discovery.json @@ -17,7 +17,9 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_user_discovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_user_discovery.json index 4437334b0aa1f..b5c5e66af6e7e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_user_discovery.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_system_user_discovery.json @@ -17,7 +17,9 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "threat": [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_dns_tunneling.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_dns_tunneling.json index 1352fde91b59b..fbce00ebaf5a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_dns_tunneling.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_dns_tunneling.json @@ -20,9 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Packetbeat" + "Network", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_dns_question.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_dns_question.json index b16e67052a212..744e9fbfc18d8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_dns_question.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_dns_question.json @@ -20,9 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Packetbeat" + "Network", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_server_domain.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_server_domain.json index a8971300fe11b..a672985655859 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_server_domain.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_server_domain.json @@ -20,9 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Packetbeat" + "Network", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_urls.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_urls.json index 469f5d741ef6e..5600fbe74c7fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_urls.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_urls.json @@ -20,9 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Packetbeat" + "Network", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_user_agent.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_user_agent.json index ebcf4f987e9de..5cce9c63fd88e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_user_agent.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_user_agent.json @@ -20,9 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Packetbeat" + "Network", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json index 385158dd6b65d..5c25b47833112 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json @@ -21,9 +21,11 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json index d0a99b32d4713..e86bc96fddae5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json @@ -21,9 +21,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_suspicious_login_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_suspicious_login_activity.json index f309debcdffe9..ae0ac2b716e32 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_suspicious_login_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_suspicious_login_activity.json @@ -20,9 +20,11 @@ "severity": "low", "tags": [ "Elastic", + "Host", "Linux", + "Threat Detection", "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_process.json index 56874ec371b43..3e35b2af5e3b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_process.json @@ -17,8 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_user.json index f124cda7717c3..7c45ab4dfa08b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_metadata_user.json @@ -17,8 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", "version": 1 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json index 0ab591097f975..937fcbc079eb8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json @@ -21,9 +21,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json index a7b309e6d7fcd..d45661f737b5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json @@ -20,9 +20,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json index bc6346f457b65..88764ed1bf46d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json @@ -21,9 +21,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json index 97351a1f517b3..2ebc7ee667d4e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json @@ -20,9 +20,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_script.json index d0dc8d7e40fa2..91714b10d2ab6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_script.json @@ -20,9 +20,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_service.json index b7e7a0357e118..3aea73eb489e5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_service.json @@ -20,9 +20,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json index 26bd6837cbde5..749cbb218bcd8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json @@ -21,9 +21,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_runas_event.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_runas_event.json index 1af765f568bb1..8ed8a005d9aa5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_runas_event.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_runas_event.json @@ -20,9 +20,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_type10_remote_login.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_type10_remote_login.json index b69e759120ce4..dda4e0bbad88d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_type10_remote_login.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_type10_remote_login.json @@ -21,9 +21,11 @@ "severity": "low", "tags": [ "Elastic", - "ML", - "Windows" + "Host", + "Windows", + "Threat Detection", + "ML" ], "type": "machine_learning", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json index c503d2298adad..0ee0bbd6d6226 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "type": "query", "version": 2 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json index d095d7c1166de..211fdb1ae3474 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "type": "query", "version": 2 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json index 2fe27575b7b2a..eb726e24c89da 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "type": "query", "version": 2 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json index 23b3313488847..682dc17f0ed49 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json @@ -24,10 +24,11 @@ "severity": "medium", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Network", - "Continuous Monitoring" + "Network Security" ], "type": "query", "version": 2 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json index 5b19031046b66..88e556d37a27c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "type": "query", "version": 2 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json index 58ba13e147a38..262a91f8e25c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json @@ -24,10 +24,11 @@ "severity": "medium", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "type": "query", "version": 2 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json index 1efcf0474c049..0101ae0459454 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json @@ -21,10 +21,11 @@ "severity": "medium", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "type": "query", "version": 2 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json index 87d5bf3e0f48c..fad3e3c922478 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json index a4c62b98fb060..077147a8ed1a6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_adobe_hijack_persistence.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_app_compat_shim.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_app_compat_shim.json index e9e2e044ddc04..5c467c39f5128 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_app_compat_shim.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_app_compat_shim.json @@ -11,13 +11,16 @@ "language": "eql", "license": "Elastic License", "name": "Installation of Custom Shim Databases", - "query": "/* dependent on wildcard for registry.value */\n\nsequence by process.entity_id with maxspan=5m\n [process where event.type in (\"start\", \"process_started\") and\n not (process.name == \"sdbinst.exe\" and process.parent.name == \"msiexec.exe\")]\n [registry where event.type in (\"creation\", \"change\") and\n wildcard(registry.path, \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Custom\\\\*.sdb\")]\n", + "query": "sequence by process.entity_id with maxspan=5m\n [process where event.type in (\"start\", \"process_started\") and\n not (process.name : \"sdbinst.exe\" and process.parent.name : \"msiexec.exe\")]\n [registry where event.type in (\"creation\", \"change\") and\n wildcard(registry.path, \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Custom\\\\*.sdb\")]\n", "risk_score": 21, "rule_id": "c5ce48a6-7f57-4ee8-9313-3d0024caee10", "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json index c1d7d51f1401e..9d1a7c7aef464 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json index 9cd9572400a6c..764c60b829498 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json index d5c9e505659f6..9003f6877341f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Monitoring", - "Continuous Monitoring" + "Monitoring" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json index 302618773e323..4fef3e833a7b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json @@ -24,10 +24,11 @@ "severity": "low", "tags": [ "Elastic", + "Identity", "Okta", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_account_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_account_created.json index 645c025ec4738..5c000967ce44d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_account_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_account_created.json @@ -22,11 +22,12 @@ "rule_id": "df26fd74-1baa-4479-b42e-48da84642330", "severity": "low", "tags": [ + "Elastic", + "Cloud", "Azure", "Continuous Monitoring", - "Elastic", - "Identity and Access", - "SecOps" + "SecOps", + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_runbook_created_or_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_runbook_created_or_modified.json index e96700e409090..28a5864353942 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_runbook_created_or_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_runbook_created_or_modified.json @@ -23,6 +23,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_webhook_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_webhook_created.json index f31fdcc18978e..5dde815022283 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_webhook_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_automation_webhook_created.json @@ -23,6 +23,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_conditional_access_policy_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_conditional_access_policy_modified.json similarity index 99% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_conditional_access_policy_modified.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_conditional_access_policy_modified.json index 8d4d4b971316e..14d6d3d479c6a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_conditional_access_policy_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_conditional_access_policy_modified.json @@ -20,6 +20,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_pim_user_added_global_admin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_pim_user_added_global_admin.json index b8ea2c55dd3f9..24411a40ffc46 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_pim_user_added_global_admin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_pim_user_added_global_admin.json @@ -22,6 +22,7 @@ "severity": "high", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_privileged_identity_management_role_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_privileged_identity_management_role_modified.json similarity index 99% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_privileged_identity_management_role_modified.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_privileged_identity_management_role_modified.json index f675a490c4e05..77e955727b2d8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_azure_privileged_identity_management_role_modified.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_azure_privileged_identity_management_role_modified.json @@ -21,6 +21,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json index 1b98b9744cd5b..8deaa9924cc1d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ec2_network_acl_creation.json @@ -27,11 +27,12 @@ "rule_id": "39144f38-5284-4f8e-a2ae-e3fd628d90b0", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Network", - "Continuous Monitoring" + "Network Security" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_service_account_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_service_account_created.json index aa9d48459262e..62e28e588cd0a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_service_account_created.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gcp_service_account_created.json @@ -22,6 +22,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "GCP", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_gpo_schtask_service_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gpo_schtask_service_creation.json similarity index 96% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_gpo_schtask_service_creation.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gpo_schtask_service_creation.json index fbf6fddcb8c00..536eda4e21476 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_gpo_schtask_service_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_gpo_schtask_service_creation.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json index 0addb86b8d031..963ac46b7ed94 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_iam_group_creation.json @@ -25,11 +25,12 @@ "rule_id": "169f3a93-efc7-4df2-94d6-0d9438c310d1", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json index e3dedeef07eb5..b29a8b2384f95 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_kernel_module_activity.json @@ -23,7 +23,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Persistence" ], "threat": [ { @@ -43,5 +46,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json index 78f035318c614..9e5a6a0eae041 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_commands.json @@ -20,7 +20,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mfa_disabled_for_azure_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_mfa_disabled_for_azure_user.json similarity index 99% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mfa_disabled_for_azure_user.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_mfa_disabled_for_azure_user.json index eda6f5b2bdf62..8a9f4d4c661e9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_mfa_disabled_for_azure_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_mfa_disabled_for_azure_user.json @@ -17,6 +17,7 @@ "severity": "medium", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json index 5899b58bce4d5..e7f4598a19f33 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json @@ -4,7 +4,8 @@ ], "description": "Windows contains accessibility features that may be launched with a key combination before a user has logged in. An adversary can modify the way these programs are launched to get a command prompt or backdoor without logging in to the system.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -15,7 +16,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json index eb77c183d90ea..06ca022726aad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_rds_cluster_creation.json @@ -27,11 +27,12 @@ "rule_id": "e14c5fd7-fdd7-49c2-9e5b-ec49d817bc8d", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Asset Visibility", - "Continuous Monitoring" + "Asset Visibility" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json index 2aaf0012acabf..ea10fa9bdf865 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_shell_activity_by_web_server.json @@ -23,7 +23,10 @@ "severity": "medium", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Persistence" ], "threat": [ { @@ -43,5 +46,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json index 32d78480325e6..880101e8d9338 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_system_shells_via_services.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json index 3f2e00f0976de..9bc603b5ad547 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_account_creation.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_application.json index 8882b87e91291..3fddde78beb33 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_application.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_application.json @@ -17,6 +17,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_service_principal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_service_principal.json index f7c0af67692e7..de6482f14d2f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_service_principal.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_user_added_as_owner_for_azure_service_principal.json @@ -20,6 +20,7 @@ "severity": "low", "tags": [ "Elastic", + "Cloud", "Azure", "Continuous Monitoring", "SecOps", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_application_shimming.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_application_shimming.json index f20cc75dfa38b..c9d56a9c68edb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_application_shimming.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_application_shimming.json @@ -4,7 +4,8 @@ ], "description": "The Application Shim was created to allow for backward compatibility of software as the operating system codebase changes over time. This Windows functionality has been abused by attackers to stealthily gain persistence and arbitrary code execution in legitimate Windows processes.", "index": [ - "winlogbeat-*" + "winlogbeat-*", + "logs-endpoint.events.*" ], "language": "kuery", "license": "Elastic License", @@ -15,7 +16,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json index b7f4ec5d8a73c..0622309387f35 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_update_orchestrator_service_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_update_orchestrator_service_hijack.json index e512e92a31560..bdddf2eb7e8c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_update_orchestrator_service_hijack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_update_orchestrator_service_hijack.json @@ -20,7 +20,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Persistence" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_service_suspicious_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_service_suspicious_file.json index 1a76e077a7465..398e4ca22b757 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_service_suspicious_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_service_suspicious_file.json @@ -21,7 +21,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_suspicious_spl_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_suspicious_spl_file.json index c5ffe5a9f6a11..18da4c8075c8f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_suspicious_spl_file.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_suspicious_spl_file.json @@ -21,7 +21,10 @@ "severity": "high", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json index 16389d43945f1..16af96e98448c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_root_login_without_mfa.json @@ -24,11 +24,12 @@ "rule_id": "bc0c6f0d-dab0-47a3-b135-0925f0a333bc", "severity": "high", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json index e72e58132adee..ff63d1e38d950 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setgid_bit_set_via_chmod.json @@ -18,7 +18,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Privilege Escalation" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json index 5e560097d2545..47490fe08ff12 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_setuid_bit_set_via_chmod.json @@ -18,7 +18,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Privilege Escalation" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json index 73a804fcbda8f..5519f6ce3a9ec 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_sudoers_file_mod.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Linux" + "Host", + "Linux", + "Threat Detection", + "Privilege Escalation" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json index b22457db49e49..80b01f90d3cf4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json index 740ff47e5abe5..415111c725828 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_event_viewer.json @@ -17,7 +17,10 @@ "severity": "low", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" ], "threat": [ { @@ -37,5 +40,5 @@ } ], "type": "query", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_sdclt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_sdclt.json new file mode 100644 index 0000000000000..ce82db3a96e11 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_sdclt.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies User Account Control (UAC) bypass via sdclt.exe. Attackers bypass UAC to stealthily execute code with elevated permissions.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Bypass UAC via Sdclt", + "query": "/* add winlogbeat-* when process.code_signature.* fields are populated */\n\nsequence with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and process.name : \"sdclt.exe\" and\n /* uncomment once in winlogbeat */\n /* process.code_signature.subject_name == \"Microsoft Corporation\" and process.code_signature.trusted == true and */\n process.args : \"/kickoffelev\"\n ] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.parent.name : \"sdclt.exe\" and\n not (process.executable : \"C:\\\\Windows\\\\System32\\\\sdclt.exe\" or\n process.executable : \"C:\\\\Windows\\\\System32\\\\control.exe\" or\n process.executable : \"C:\\\\Windows\\\\SysWOW64\\\\sdclt.exe\" or\n process.executable : \"C:\\\\Windows\\\\SysWOW64\\\\control.exe\")\n ] by process.parent.entity_id\n", + "risk_score": 21, + "rule_id": "9b54e002-034a-47ac-9307-ad12c03fa900", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1088", + "name": "Bypass User Account Control", + "reference": "https://attack.mitre.org/techniques/T1088/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json index 0cac8561c7e9c..ad871716a67aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json @@ -17,7 +17,10 @@ "severity": "medium", "tags": [ "Elastic", - "Windows" + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json index 55947e00170ae..3d1e66497bd2e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_updateassumerolepolicy.json @@ -24,11 +24,12 @@ "rule_id": "a60326d7-dca7-4fb7-93eb-1ca03a1febbd", "severity": "low", "tags": [ - "AWS", "Elastic", + "Cloud", + "AWS", + "Continuous Monitoring", "SecOps", - "Identity and Access", - "Continuous Monitoring" + "Identity and Access" ], "threat": [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json index 1e2f217751e96..ed9356f46501c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json @@ -1,5 +1,5 @@ { - "name": "Query with a threat mapping", + "name": "Query with a indicator mapping", "description": "Query with a threat mapping", "rule_id": "threat-mapping", "risk_score": 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index 7b7c40f0c4355..c4869f024a977 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -105,7 +105,9 @@ describe('singleSearchAfter', () => { timestampOverride: undefined, buildRuleMessage, }); - expect(searchErrors).toEqual(['reason: some reason, type: some type, caused by: some reason']); + expect(searchErrors).toEqual([ + 'reason: "some reason" type: "some type" caused by reason: "some reason" caused by type: "some type"', + ]); }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 157f741439bd2..894e934ff0247 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -878,7 +878,7 @@ describe('utils', () => { ]; const createdErrors = createErrorsFromShard({ errors }); expect(createdErrors).toEqual([ - 'reason: some reason, type: some type, caused by: some reason', + 'reason: "some reason" type: "some type" caused by reason: "some reason" caused by type: "some type"', ]); }); @@ -917,8 +917,54 @@ describe('utils', () => { ]; const createdErrors = createErrorsFromShard({ errors }); expect(createdErrors).toEqual([ - 'reason: some reason, type: some type, caused by: some reason', - 'reason: some reason 2, type: some type 2, caused by: some reason 2', + 'reason: "some reason" type: "some type" caused by reason: "some reason" caused by type: "some type"', + 'reason: "some reason 2" type: "some type 2" caused by reason: "some reason 2" caused by type: "some type 2"', + ]); + }); + + test('You can have missing values for the shard errors and get the expected output of an empty string', () => { + const errors: ShardError[] = [ + { + shard: 1, + index: 'index-123', + node: 'node-123', + reason: {}, + }, + ]; + const createdErrors = createErrorsFromShard({ errors }); + expect(createdErrors).toEqual(['']); + }); + + test('You can have a single value for the shard errors and get expected output without extra spaces anywhere', () => { + const errors: ShardError[] = [ + { + shard: 1, + index: 'index-123', + node: 'node-123', + reason: { + reason: 'some reason something went wrong', + }, + }, + ]; + const createdErrors = createErrorsFromShard({ errors }); + expect(createdErrors).toEqual(['reason: "some reason something went wrong"']); + }); + + test('You can have two values for the shard errors and get expected output with one space exactly between the two values', () => { + const errors: ShardError[] = [ + { + shard: 1, + index: 'index-123', + node: 'node-123', + reason: { + reason: 'some reason something went wrong', + caused_by: { type: 'some type' }, + }, + }, + ]; + const createdErrors = createErrorsFromShard({ errors }); + expect(createdErrors).toEqual([ + 'reason: "some reason something went wrong" caused by type: "some type"', ]); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index ac10f5ed9a72d..e2b39b8d0a8c8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -511,7 +511,23 @@ export const getSignalTimeTuples = ({ */ export const createErrorsFromShard = ({ errors }: { errors: ShardError[] }): string[] => { return errors.map((error) => { - return `reason: ${error.reason.reason}, type: ${error.reason.caused_by.type}, caused by: ${error.reason.caused_by.reason}`; + const { + reason: { + reason, + type, + caused_by: { reason: causedByReason, type: causedByType } = { + reason: undefined, + type: undefined, + }, + } = {}, + } = error; + + return [ + ...(reason != null ? [`reason: "${reason}"`] : []), + ...(type != null ? [`type: "${type}"`] : []), + ...(causedByReason != null ? [`caused by reason: "${causedByReason}"`] : []), + ...(causedByType != null ? [`caused by type: "${causedByType}"`] : []), + ].join(' '); }); }; diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index 29db38bbbea68..c735412aedbf5 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -48,21 +48,26 @@ export interface ShardsResponse { failures?: ShardError[]; } -export interface ShardError { +/** + * This type is being very conservative with the partials to not expect anything to + * be guaranteed on the type as we don't have regular and proper types of ShardError. + * Once we do, remove this type for the regular ShardError type from the elastic library. + */ +export type ShardError = Partial<{ shard: number; index: string; node: string; - reason: { + reason: Partial<{ type: string; reason: string; index_uuid: string; index: string; - caused_by: { + caused_by: Partial<{ type: string; reason: string; - }; - }; -} + }>; + }>; +}>; export interface SearchResponse { took: number; diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index 5cf17af2fa9c0..6387839db3bfe 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -172,7 +172,7 @@ export const getMlJobsUsage = async (ml: MlPluginSetup | undefined): Promise { const serverBasePath = http.basePath.serverBasePath; - const path = request.url.pathname!; + const path = request.url.pathname; const spaceId = spacesService.getSpaceId(request); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 6408803c2114b..a3335b1e075f2 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -9,8 +9,6 @@ import { LifecycleResponseFactory, CoreSetup, } from 'src/core/server'; -import { format } from 'url'; -import { modifyUrl } from '../utils/url'; import { getSpaceIdFromPath } from '../../../common'; export interface OnRequestInterceptorDeps { @@ -34,16 +32,9 @@ export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDep http.basePath.set(request, reqBasePath); - const newLocation = (path && path.substr(reqBasePath.length)) || '/'; + const newPathname = path.substr(reqBasePath.length) || '/'; - const newUrl = modifyUrl(format(request.url), (parts) => { - return { - ...parts, - pathname: newLocation, - }; - }); - - return toolkit.rewriteUrl(newUrl); + return toolkit.rewriteUrl(`${newPathname}${request.url.search}`); } return toolkit.next(); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index b48bf971d0c1b..d1e1d81134940 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -58,7 +58,7 @@ const createService = async (serverBasePath: string = '') => { serverBasePath, } as HttpServiceSetup['basePath']; httpSetup.basePath.get = jest.fn().mockImplementation((request: KibanaRequest) => { - const { spaceId } = getSpaceIdFromPath(request.url.path); + const { spaceId } = getSpaceIdFromPath(request.url.pathname); if (spaceId !== DEFAULT_SPACE_ID) { return `/s/${spaceId}`; @@ -83,7 +83,7 @@ describe('SpacesService', () => { const spacesServiceSetup = await createService(); const request: KibanaRequest = { - url: { path: '/app/kibana' }, + url: { pathname: '/app/kibana' }, } as KibanaRequest; expect(spacesServiceSetup.getSpaceId(request)).toEqual(DEFAULT_SPACE_ID); @@ -93,7 +93,7 @@ describe('SpacesService', () => { const spacesServiceSetup = await createService(); const request: KibanaRequest = { - url: { path: '/s/foo/app/kibana' }, + url: { pathname: '/s/foo/app/kibana' }, } as KibanaRequest; expect(spacesServiceSetup.getSpaceId(request)).toEqual('foo'); @@ -140,7 +140,7 @@ describe('SpacesService', () => { const spacesServiceSetup = await createService(); const request: KibanaRequest = { - url: { path: '/app/kibana' }, + url: { pathname: '/app/kibana' }, } as KibanaRequest; expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(true); @@ -150,7 +150,7 @@ describe('SpacesService', () => { const spacesServiceSetup = await createService(); const request: KibanaRequest = { - url: { path: '/s/foo/app/kibana' }, + url: { pathname: '/s/foo/app/kibana' }, } as KibanaRequest; expect(spacesServiceSetup.isInDefaultSpace(request)).toEqual(false); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts index e33a3e775ca96..2f0cf3cbbcd16 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { Writable } from '@kbn/utility-types'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { getAlertType } from './alert_type'; import { Params } from './alert_type_params'; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts index c209894fb6e89..3b771403b8b1a 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts @@ -7,6 +7,7 @@ import { ParamsSchema, Params } from './alert_type_params'; import { runTests } from './lib/core_query_types.test'; import { TypeOf } from '@kbn/config-schema'; +import type { Writable } from '@kbn/utility-types'; const DefaultParams: Writable> = { index: 'index-name', diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.test.ts index 6c9c3542aea03..be8e61eac8cb4 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.test.ts @@ -7,6 +7,7 @@ // tests of common properties on time_series_query and alert_type_params import { ObjectType } from '@kbn/config-schema'; +import type { Writable } from '@kbn/utility-types'; import { CoreQueryParams } from './core_query_types'; import { MAX_GROUPS } from '../index'; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.test.ts index ec164122032cb..58f7652eaf074 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { Writable } from '@kbn/utility-types'; import { TimeSeriesQuerySchema, TimeSeriesQuery } from './time_series_types'; import { runTests } from './core_query_types.test'; import { TypeOf } from '@kbn/config-schema'; diff --git a/x-pack/plugins/task_manager/server/README.md b/x-pack/plugins/task_manager/README.md similarity index 95% rename from x-pack/plugins/task_manager/server/README.md rename to x-pack/plugins/task_manager/README.md index fd2409a7db0a5..b25d3cc49f980 100644 --- a/x-pack/plugins/task_manager/server/README.md +++ b/x-pack/plugins/task_manager/README.md @@ -1,7 +1,8 @@ # Kibana task manager -The task manager is a generic system for running background tasks. It supports: +The task manager is a generic system for running background tasks. +It supports: - Single-run and recurring tasks - Scheduling tasks to run after a specified datetime - Basic retry logic @@ -48,6 +49,10 @@ The task_manager can be configured via `taskManager` config options (e.g. `taskM - `override_num_workers`: An object of `taskType: number` that overrides the `num_workers` for tasks - For example: `task_manager.override_num_workers.reporting: 2` would override the number of workers occupied by tasks of type `reporting` - This allows sysadmins to tweak the operational performance of Kibana, allowing more or fewer tasks of a specific type to run simultaneously +- `monitored_aggregated_stats_refresh_rate` - Dictates how often we refresh the "Cold" metrics. Learn More: [./MONITORING](./MONITORING.MD) +- `monitored_stats_running_average_window`- Dictates the size of the window used to calculate the running average of various "Hot" stats. Learn More: [./MONITORING](./MONITORING.MD) +- `monitored_stats_required_freshness` - Dictates the _required freshness_ of critical "Hot" stats. Learn More: [./MONITORING](./MONITORING.MD) +- `monitored_task_execution_thresholds`- Dictates the threshold of failed task executions. Learn More: [./MONITORING](./MONITORING.MD) ## Task definitions @@ -460,3 +465,9 @@ The task manager's public API is create / delete / list. Updates aren't directly node scripts/functional_tests_server.js --config x-pack/test/plugin_api_integration/config.ts node scripts/functional_test_runner --config x-pack/test/plugin_api_integration/config.ts ``` + +## Monitoring + +Task Manager exposes runtime statistics which enable basic observability into its inner workings and makes it possible to monitor the system from external services. + +Learn More: [./MONITORING](./MONITORING.MD) \ No newline at end of file diff --git a/x-pack/plugins/task_manager/server/MONITORING.md b/x-pack/plugins/task_manager/server/MONITORING.md new file mode 100644 index 0000000000000..4960086411e9a --- /dev/null +++ b/x-pack/plugins/task_manager/server/MONITORING.md @@ -0,0 +1,265 @@ +# Task Manager Monitoring + +Task Manager has an internal monitoring mechanism in which keeps track of a variety of metrics which are exposed via a `health` api endpoint and Kibana Server Log debug messaging. + +## Exposed Metrics +There are three different sections to the stats returned by the `health` api. +- `configuration`: Summarizes Task Manager's current configuration. +- `workload`: Summarizes the workload in the current deployment. +- `runtime`: Tracks Task Manager's performance. + +### Configuring the Stats +There are four new configurations: + +- `xpack.task_manager.monitored_stats_required_freshness` - The _required freshness_ of critical "Hot" stats, which means that if key stats (last polling cycle time, for example) haven't been refreshed within the specified duration, the `_health` endpoint and service will report an `Error` status. By default this is inferred from the configured `poll_interval` and is set to `poll_interval` plus a `1s` buffer. +- `xpack.task_manager.monitored_aggregated_stats_refresh_rate` - Dictates how often we refresh the "Cold" metrics. These metrics require an aggregation against Elasticsearch and add load to the system, hence we want to limit how often we execute these. We also inffer the _required freshness_ of these "Cold" metrics from this configuration, which means that if these stats have not been updated within the required duration then the `_health` endpoint and service will report an `Error` status. This covers the entire `workload` section of the stats. By default this is configured to `60s`, and as a result the _required freshness_ defaults to `61s` (refresh plus a `1s` buffer). +- `xpack.task_manager.monitored_stats_running_average_window`- Dictates the size of the window used to calculate the running average of various "Hot" stats, such as the time it takes to run a task, the _drift_ that tasks experience etc. These stats are collected throughout the lifecycle of tasks and this window will dictate how large the queue we keep in memory would be, and how many values we need to calculate the average against. We do not calculate the average on *every* new value, but rather only when the time comes to summarize the stats before logging them or returning them to the API endpoint. +- `xpack.task_manager.monitored_task_execution_thresholds`- Configures the threshold of failed task executions at which point the `warn` or `error` health status will be set either at a default level or a custom level for specific task types. This will allow you to mark the health as `error` when any task type failes 90% of the time, but set it to `error` at 50% of the time for task types that you consider critical. This value can be set to any number between 0 to 100, and a threshold is hit when the value *exceeds* this number. This means that you can avoid setting the status to `error` by setting the threshold at 100, or hit `error` the moment any task failes by setting the threshold to 0 (as it will exceed 0 once a single failer occurs). + +For example, in your `Kibana.yml`: +``` +xpack.task_manager.monitored_stats_required_freshness: 5000 +xpack.task_manager.monitored_aggregated_stats_refresh_rate: 60000 +xpack.task_manager.monitored_stats_running_average_window: 50 +xpack.task_manager.monitored_task_execution_thresholds: + default: + error_threshold: 70 + warn_threshold: 50 + custom: + "alerting:always-firing": + error_threshold: 50 + warn_threshold: 0 +``` + +## Consuming Health Stats +Task Manager exposes a `/api/task_manager/_health` api which returns the _latest_ stats. +Calling this API is designed to be fast and doesn't actually perform any checks- rather it returns the result of the latest stats in the system, and is design in such a way that you could call it from an external service on a regular basis without worrying that you'll be adding substantial load to the system. + +Additionally, the metrics are logged out into Task Manager's `DEBUG` logger at a regular cadence (dictated by the Polling Interval). +If you wish to enable DEBUG logging in your Kibana instance, you will need to add the following to your `Kibana.yml`: +``` +logging: + loggers: + - context: plugins.taskManager + appenders: [console] + level: debug +``` + +Please bear in mind that these stats are logged as often as your `poll_interval` configuration, which means it could add substantial noise to your logs. +We would recommend only enabling this level of logging temporarily. + +### Understanding the Exposed Stats + +As mentioned above, the `health` api exposes three sections: `configuration`, `workload` and `runtime`. +Each section has a `timestamp` and a `status` which indicates when the last update to this setion took place and whether the health of this section was evaluated as `OK`, `Warning` or `Error`. + +The root has its own `status` which indicate the state of the system overall as infered from the `status` of the section. +An `Error` status in any section will cause the whole system to display as `Error`. +A `Warning` status in any section will cause the whole system to display as `Warning`. +An `OK` status will only be displayed when all sections are marked as `OK`. + +The root `timestamp` is the time in which the summary was exposed (either to the DEBUG logger or the http api) and the `last_update` is the last time any one of the sections was updated. + +#### The Configuration Section +The `configuration` section summarizes Task Manager's current configuration, including dynamic configurations which change over time, such as `poll_interval` and `max_workers` which adjust in reaction to changing load on the system. + +These are "Hot" stats which are updated whenever a change happens in the configuration. + +#### The Workload Section +The `workload` which summarizes the work load in the current deployment, listing the tasks in the system, their types and what their current status is. + +It includes three sub sections: + - The number of tasks scheduled in the system, broken down by type and status. + - The number of idle `overdue` tasks, whose `runAt` has expired. + - Execution density in the next minute or so (configurable), which shows how many tasks are scheduled to execute in the scope of each polling interval. This can give us an idea of how much load there is on the current Kibana deployment. + +These are "Cold" stat which are updated at a regular cadence, configured by the `monitored_aggregated_stats_refresh_rate` config. + +#### The Runtime Section +The `runtime` tracks Task Manager's performance as it runs, making note of task execution time, _drift_ etc. +These include: + - The time it takes a task to run (mean and median, using a configurable running average window, `50` by default) + - The average _drift_ that tasks experience (mean and median, using the same configurable running average window as above). Drift tells us how long after a task's scheduled a task typically executes. + - The polling rate (the timestamp of the last time a polling cycle completed) and the result [`No tasks | Filled task pool | Unexpectedly ran out of workers`] frequency the past 50 polling cycles (using the same window size as the one used for running averages) + - The `Success | Retry | Failure ratio` by task type. This is different than the workload stats which tell you what's in the queue, but ca't keep track of retries and of non recurring tasks as they're wiped off the index when completed. + +These are "Hot" stats which are updated reactively as Tasks are executed and interacted with. + +### Example Stats + +For example, if you _curl_ the `/api/task_manager/_health` endpoint, you might get these stats: +``` +{ + /* the time these stats were returned by the api */ + "timestamp": "2020-10-05T18:26:11.346Z", + /* the overall status of the system */ + "status": "OK", + /* last time any stat was updated in this output */ + "last_update": "2020-10-05T17:57:55.411Z", + "stats": { + "configuration": { /* current configuration of TM */ + "timestamp": "2020-10-05T17:56:06.507Z", + "status": "OK", + "value": { + "max_workers": 10, + "poll_interval": 3000, + "request_capacity": 1000, + "max_poll_inactivity_cycles": 10, + "monitored_aggregated_stats_refresh_rate": 60000, + "monitored_stats_running_average_window": 50 + } + }, + "workload": { /* The workload of this deployment */ + "timestamp": "2020-10-05T17:57:06.534Z", + "status": "OK", + "value": { + "count": 6, /* count of tasks in the system */ + "task_types": { /* what tasks are there and what status are they in */ + "actions_telemetry": { + "count": 1, + "status": { + "idle": 1 + } + }, + "alerting_telemetry": { + "count": 1, + "status": { + "idle": 1 + } + }, + "apm-telemetry-task": { + "count": 1, + "status": { + "idle": 1 + } + }, + "endpoint:user-artifact-packager": { + "count": 1, + "status": { + "idle": 1 + } + }, + "lens_telemetry": { + "count": 1, + "status": { + "idle": 1 + } + }, + "session_cleanup": { + "count": 1, + "status": { + "idle": 1 + } + } + }, + + /* Frequency of recurring tasks schedules */ + "schedule": [ + ["60s", 1], /* 1 task, every 60s */ + ["3600s", 3], /* 3 tasks every hour */ + ["720m", 1] + ], + /* There are no overdue tasks in this system at the moment */ + "overdue": 0, + /* This is the schedule density, it shows a histogram of all the polling intervals in the next minute (or, if + pollInterval is configured unusually high it will show a min of 2 refresh intervals into the future, and a max of 50 buckets). + Here we see that on the 3rd polling interval from *now* (which is ~9 seconds from now, as pollInterval is `3s`) there is one task due to run. + We also see that there are 5 due two intervals later, which is fine as we have a max workers of `10` + */ + "estimated_schedule_density": [0, 0, 1, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + } + }, + "runtime": { + "timestamp": "2020-10-05T17:57:55.411Z", + "status": "OK", + "value": { + "polling": { + /* When was the last polling cycle? */ + "last_successful_poll": "2020-10-05T17:57:55.411Z", + /* What is the frequency of polling cycle result? + Here we see 94% of "NoTasksClaimed" and 6% "PoolFilled" */ + "result_frequency_percent_as_number": { + "NoTasksClaimed": 94, + "RanOutOfCapacity": 0, /* This is a legacy result, we might want to rename - it tells us when a polling cycle resulted in claiming more tasks than we had workers for, butt he name doesn't make much sense outside of the context of the code */ + "PoolFilled": 6 + } + }, + /* on average, the tasks in this deployment run 1.7s after their scheduled time */ + "drift": { + "mean": 1720, + "median": 2276 + }, + "execution": { + "duration": { + /* on average, the `endpoint:user-artifact-packager` tasks take 15ms to run */ + "endpoint:user-artifact-packager": { + "mean": 15, + "median": 14.5 + }, + "session_cleanup": { + "mean": 28, + "median": 28 + }, + "lens_telemetry": { + "mean": 100, + "median": 100 + }, + "actions_telemetry": { + "mean": 135, + "median": 135 + }, + "alerting_telemetry": { + "mean": 197, + "median": 197 + }, + "apm-telemetry-task": { + "mean": 1347, + "median": 1347 + } + }, + "result_frequency_percent_as_number": { + /* and 100% of `endpoint:user-artifact-packager` have completed in success (within the running average window, so the past 50 runs (by default, configrable by `monitored_stats_running_average_window`) */ + "endpoint:user-artifact-packager": { + "status": "OK", + "Success": 100, + "RetryScheduled": 0, + "Failed": 0 + }, + "session_cleanup": { + /* `error` status as 90% of results are `Failed` */ + "status": "error", + "Success": 5, + "RetryScheduled": 5, + "Failed": 90 + }, + "lens_telemetry": { + "status": "OK", + "Success": 100, + "RetryScheduled": 0, + "Failed": 0 + }, + "actions_telemetry": { + "status": "OK", + "Success": 100, + "RetryScheduled": 0, + "Failed": 0 + }, + "alerting_telemetry": { + "status": "OK", + "Success": 100, + "RetryScheduled": 0, + "Failed": 0 + }, + "apm-telemetry-task": { + "status": "OK", + "Success": 100, + "RetryScheduled": 0, + "Failed": 0 + } + } + } + } + } + } +} +``` diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index d5bbbe65582f1..d2d5ac8f22a1f 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -15,6 +15,16 @@ describe('config validation', () => { "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, + "monitored_aggregated_stats_refresh_rate": 60000, + "monitored_stats_required_freshness": 4000, + "monitored_stats_running_average_window": 50, + "monitored_task_execution_thresholds": Object { + "custom": Object {}, + "default": Object { + "error_threshold": 90, + "warn_threshold": 80, + }, + }, "poll_interval": 3000, "request_capacity": 1000, } @@ -31,4 +41,147 @@ describe('config validation', () => { `"[index]: \\".tasks\\" is an invalid Kibana Task Manager index, as it is already in use by the ElasticSearch Tasks Manager"` ); }); + + test('the required freshness of the monitored stats config must always be less-than-equal to the poll interval', () => { + const config: Record = { + monitored_stats_required_freshness: 100, + }; + expect(() => { + configSchema.validate(config); + }).toThrowErrorMatchingInlineSnapshot( + `"The specified monitored_stats_required_freshness (100) is invalid, as it is below the poll_interval (3000)"` + ); + }); + + test('the default required freshness of the monitored stats is poll interval with a slight buffer', () => { + const config: Record = {}; + expect(configSchema.validate(config)).toMatchInlineSnapshot(` + Object { + "enabled": true, + "index": ".kibana_task_manager", + "max_attempts": 3, + "max_poll_inactivity_cycles": 10, + "max_workers": 10, + "monitored_aggregated_stats_refresh_rate": 60000, + "monitored_stats_required_freshness": 4000, + "monitored_stats_running_average_window": 50, + "monitored_task_execution_thresholds": Object { + "custom": Object {}, + "default": Object { + "error_threshold": 90, + "warn_threshold": 80, + }, + }, + "poll_interval": 3000, + "request_capacity": 1000, + } + `); + }); + + test('the custom monitored_task_execution_thresholds can be configured at task type', () => { + const config: Record = { + monitored_task_execution_thresholds: { + custom: { + 'alerting:always-fires': { + error_threshold: 50, + warn_threshold: 30, + }, + }, + }, + }; + expect(configSchema.validate(config)).toMatchInlineSnapshot(` + Object { + "enabled": true, + "index": ".kibana_task_manager", + "max_attempts": 3, + "max_poll_inactivity_cycles": 10, + "max_workers": 10, + "monitored_aggregated_stats_refresh_rate": 60000, + "monitored_stats_required_freshness": 4000, + "monitored_stats_running_average_window": 50, + "monitored_task_execution_thresholds": Object { + "custom": Object { + "alerting:always-fires": Object { + "error_threshold": 50, + "warn_threshold": 30, + }, + }, + "default": Object { + "error_threshold": 90, + "warn_threshold": 80, + }, + }, + "poll_interval": 3000, + "request_capacity": 1000, + } + `); + }); + + test('the monitored_task_execution_thresholds ensures that the default warn_threshold is lt the default error_threshold', () => { + const config: Record = { + monitored_task_execution_thresholds: { + default: { + warn_threshold: 80, + error_threshold: 70, + }, + }, + }; + expect(() => { + configSchema.validate(config); + }).toThrowErrorMatchingInlineSnapshot( + `"[monitored_task_execution_thresholds.default]: warn_threshold (80) must be less than, or equal to, error_threshold (70)"` + ); + }); + + test('the monitored_task_execution_thresholds allows the default warn_threshold to equal the default error_threshold', () => { + const config: Record = { + monitored_task_execution_thresholds: { + default: { + warn_threshold: 70, + error_threshold: 70, + }, + }, + }; + expect(() => { + configSchema.validate(config); + }).not.toThrowError(); + }); + + test('the monitored_task_execution_thresholds ensures that the warn_threshold is lte error_threshold on custom thresholds', () => { + const config: Record = { + monitored_task_execution_thresholds: { + custom: { + 'alerting:always-fires': { + error_threshold: 80, + warn_threshold: 90, + }, + }, + }, + }; + expect(() => { + configSchema.validate(config); + }).toThrowErrorMatchingInlineSnapshot( + `"[monitored_task_execution_thresholds.custom.alerting:always-fires]: warn_threshold (90) must be less than, or equal to, error_threshold (80)"` + ); + }); + + test('the monitored_task_execution_thresholds allows a custom error_threshold which is lower than the default warn_threshold', () => { + const config: Record = { + monitored_task_execution_thresholds: { + default: { + warn_threshold: 80, + error_threshold: 90, + }, + custom: { + 'alerting:always-fires': { + error_threshold: 60, + warn_threshold: 50, + }, + }, + }, + }; + expect(() => { + configSchema.validate(config); + }).not.toThrowError(); + }); }); diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index aa78cf3baa96d..157f01281836d 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -10,44 +10,109 @@ export const DEFAULT_MAX_WORKERS = 10; export const DEFAULT_POLL_INTERVAL = 3000; export const DEFAULT_MAX_POLL_INACTIVITY_CYCLES = 10; -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), - /* The maximum number of times a task will be attempted before being abandoned as failed */ - max_attempts: schema.number({ - defaultValue: 3, - min: 1, - }), - /* How often, in milliseconds, the task manager will look for more work. */ - poll_interval: schema.number({ - defaultValue: DEFAULT_POLL_INTERVAL, - min: 100, - }), - /* How many poll interval cycles can work take before it's timed out. */ - max_poll_inactivity_cycles: schema.number({ - defaultValue: DEFAULT_MAX_POLL_INACTIVITY_CYCLES, - min: 1, - }), - /* How many requests can Task Manager buffer before it rejects new requests. */ - request_capacity: schema.number({ - // a nice round contrived number, feel free to change as we learn how it behaves - defaultValue: 1000, - min: 1, - }), - /* The name of the index used to store task information. */ - index: schema.string({ - defaultValue: '.kibana_task_manager', - validate: (val) => { - if (val.toLowerCase() === '.tasks') { - return `"${val}" is an invalid Kibana Task Manager index, as it is already in use by the ElasticSearch Tasks Manager`; +// Monitoring Constants +// =================== +// Refresh aggregated monitored stats at a default rate of once a minute +export const DEFAULT_MONITORING_REFRESH_RATE = 60 * 1000; +export const DEFAULT_MONITORING_STATS_RUNNING_AVERGAE_WINDOW = 50; + +export const taskExecutionFailureThresholdSchema = schema.object( + { + error_threshold: schema.number({ + defaultValue: 90, + min: 0, + }), + warn_threshold: schema.number({ + defaultValue: 80, + min: 0, + }), + }, + { + validate(config) { + if (config.error_threshold < config.warn_threshold) { + return `warn_threshold (${config.warn_threshold}) must be less than, or equal to, error_threshold (${config.error_threshold})`; + } + }, + } +); + +export const configSchema = schema.object( + { + enabled: schema.boolean({ defaultValue: true }), + /* The maximum number of times a task will be attempted before being abandoned as failed */ + max_attempts: schema.number({ + defaultValue: 3, + min: 1, + }), + /* How often, in milliseconds, the task manager will look for more work. */ + poll_interval: schema.number({ + defaultValue: DEFAULT_POLL_INTERVAL, + min: 100, + }), + /* How many poll interval cycles can work take before it's timed out. */ + max_poll_inactivity_cycles: schema.number({ + defaultValue: DEFAULT_MAX_POLL_INACTIVITY_CYCLES, + min: 1, + }), + /* How many requests can Task Manager buffer before it rejects new requests. */ + request_capacity: schema.number({ + // a nice round contrived number, feel free to change as we learn how it behaves + defaultValue: 1000, + min: 1, + }), + /* The name of the index used to store task information. */ + index: schema.string({ + defaultValue: '.kibana_task_manager', + validate: (val) => { + if (val.toLowerCase() === '.tasks') { + return `"${val}" is an invalid Kibana Task Manager index, as it is already in use by the ElasticSearch Tasks Manager`; + } + }, + }), + /* The maximum number of tasks that this Kibana instance will run simultaneously. */ + max_workers: schema.number({ + defaultValue: DEFAULT_MAX_WORKERS, + // disable the task manager rather than trying to specify it with 0 workers + min: 1, + }), + /* The rate at which we emit fresh monitored stats. By default we'll use the poll_interval (+ a slight buffer) */ + monitored_stats_required_freshness: schema.number({ + defaultValue: (config?: unknown) => + ((config as { poll_interval: number })?.poll_interval ?? DEFAULT_POLL_INTERVAL) + 1000, + min: 100, + }), + /* The rate at which we refresh monitored stats that require aggregation queries against ES. */ + monitored_aggregated_stats_refresh_rate: schema.number({ + defaultValue: DEFAULT_MONITORING_REFRESH_RATE, + /* don't run monitored stat aggregations any faster than once every 5 seconds */ + min: 5000, + }), + /* The size of the running average window for monitored stats. */ + monitored_stats_running_average_window: schema.number({ + defaultValue: DEFAULT_MONITORING_STATS_RUNNING_AVERGAE_WINDOW, + max: 100, + min: 10, + }), + /* Task Execution result warn & error thresholds. */ + monitored_task_execution_thresholds: schema.object({ + default: taskExecutionFailureThresholdSchema, + custom: schema.recordOf(schema.string(), taskExecutionFailureThresholdSchema, { + defaultValue: {}, + }), + }), + }, + { + validate: (config) => { + if ( + config.monitored_stats_required_freshness && + config.poll_interval && + config.monitored_stats_required_freshness < config.poll_interval + ) { + return `The specified monitored_stats_required_freshness (${config.monitored_stats_required_freshness}) is invalid, as it is below the poll_interval (${config.poll_interval})`; } }, - }), - /* The maximum number of tasks that this Kibana instance will run simultaneously. */ - max_workers: schema.number({ - defaultValue: DEFAULT_MAX_WORKERS, - // disable the task manager rather than trying to specify it with 0 workers - min: 1, - }), -}); + } +); export type TaskManagerConfig = TypeOf; +export type TaskExecutionFailureThreshold = TypeOf; diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index 11f6ccc881850..01326c73bd680 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -30,7 +30,17 @@ describe('managed configuration', () => { max_attempts: 9, poll_interval: 3000, max_poll_inactivity_cycles: 10, + monitored_aggregated_stats_refresh_rate: 60000, + monitored_stats_required_freshness: 4000, + monitored_stats_running_average_window: 50, request_capacity: 1000, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, }); logger = context.logger.get('taskManager'); diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts index 6df5b064f2792..4de92ffc77030 100644 --- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts +++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from 'src/core/server'; import { map } from 'lodash'; import { Subject, race, from } from 'rxjs'; import { bufferWhen, filter, bufferCount, flatMap, mapTo, first } from 'rxjs/operators'; import { either, Result, asOk, asErr, Ok, Err } from './result_type'; -import { Logger } from '../../../../../src/core/server'; export interface BufferOptions { bufferMaxDuration?: number; diff --git a/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.ts b/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.ts index 9e5f4b7c143a2..a15682a9d3f38 100644 --- a/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.ts +++ b/x-pack/plugins/task_manager/server/lib/correct_deprecated_fields.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from 'src/core/server'; import { TaskInstance, TaskInstanceWithDeprecatedFields } from '../task'; -import { Logger } from '../../../../../src/core/server'; export function ensureDeprecatedFieldsAreCorrected( { id, taskType, interval, schedule, ...taskInstance }: TaskInstanceWithDeprecatedFields, diff --git a/x-pack/plugins/task_manager/server/lib/intervals.test.ts b/x-pack/plugins/task_manager/server/lib/intervals.test.ts index ac28b81eaf490..e79694915f926 100644 --- a/x-pack/plugins/task_manager/server/lib/intervals.test.ts +++ b/x-pack/plugins/task_manager/server/lib/intervals.test.ts @@ -7,13 +7,13 @@ import _ from 'lodash'; import sinon from 'sinon'; import { - assertValidInterval, + parseIntervalAsSecond, + parseIntervalAsMillisecond, intervalFromNow, intervalFromDate, - minutesFromNow, - minutesFromDate, secondsFromNow, secondsFromDate, + asInterval, } from './intervals'; let fakeTimer: sinon.SinonFakeTimers; @@ -25,32 +25,100 @@ beforeAll(() => { afterAll(() => fakeTimer.restore()); describe('taskIntervals', () => { - describe('assertValidInterval', () => { + describe('parseIntervalAsSecond', () => { test('it accepts intervals in the form `Nm`', () => { - expect(() => assertValidInterval(`${_.random(1, 1000)}m`)).not.toThrow(); + expect(() => parseIntervalAsSecond(`${_.random(1, 1000)}m`)).not.toThrow(); }); test('it accepts intervals in the form `Ns`', () => { - expect(() => assertValidInterval(`${_.random(1, 1000)}s`)).not.toThrow(); + expect(() => parseIntervalAsSecond(`${_.random(1, 1000)}s`)).not.toThrow(); }); test('it rejects 0 based intervals', () => { - expect(() => assertValidInterval('0m')).toThrow( + expect(() => parseIntervalAsSecond('0m')).toThrow( /Invalid interval "0m"\. Intervals must be of the form {number}m. Example: 5m/ ); - expect(() => assertValidInterval('0s')).toThrow( + expect(() => parseIntervalAsSecond('0s')).toThrow( /Invalid interval "0s"\. Intervals must be of the form {number}m. Example: 5m/ ); }); test('it rejects intervals are not of the form `Nm` or `Ns`', () => { - expect(() => assertValidInterval(`5m 2s`)).toThrow( + expect(() => parseIntervalAsSecond(`5m 2s`)).toThrow( /Invalid interval "5m 2s"\. Intervals must be of the form {number}m. Example: 5m/ ); - expect(() => assertValidInterval(`hello`)).toThrow( + expect(() => parseIntervalAsSecond(`hello`)).toThrow( /Invalid interval "hello"\. Intervals must be of the form {number}m. Example: 5m/ ); }); + + test('returns an interval as s', () => { + expect(parseIntervalAsSecond('5s')).toEqual(5); + expect(parseIntervalAsSecond('15s')).toEqual(15); + expect(parseIntervalAsSecond('20m')).toEqual(20 * 60); + expect(parseIntervalAsSecond('61m')).toEqual(61 * 60); + expect(parseIntervalAsSecond('90m')).toEqual(90 * 60); + }); + }); + + describe('parseIntervalAsMillisecond', () => { + test('it accepts intervals in the form `Nm`', () => { + expect(() => parseIntervalAsMillisecond(`${_.random(1, 1000)}m`)).not.toThrow(); + }); + + test('it accepts intervals in the form `Ns`', () => { + expect(() => parseIntervalAsMillisecond(`${_.random(1, 1000)}s`)).not.toThrow(); + }); + + test('it rejects 0 based intervals', () => { + expect(() => parseIntervalAsMillisecond('0m')).toThrow( + /Invalid interval "0m"\. Intervals must be of the form {number}m. Example: 5m/ + ); + expect(() => parseIntervalAsMillisecond('0s')).toThrow( + /Invalid interval "0s"\. Intervals must be of the form {number}m. Example: 5m/ + ); + }); + + test('it rejects intervals are not of the form `Nm` or `Ns`', () => { + expect(() => parseIntervalAsMillisecond(`5m 2s`)).toThrow( + /Invalid interval "5m 2s"\. Intervals must be of the form {number}m. Example: 5m/ + ); + expect(() => parseIntervalAsMillisecond(`hello`)).toThrow( + /Invalid interval "hello"\. Intervals must be of the form {number}m. Example: 5m/ + ); + }); + + test('returns an interval as ms', () => { + expect(parseIntervalAsMillisecond('5s')).toEqual(5 * 1000); + expect(parseIntervalAsMillisecond('15s')).toEqual(15 * 1000); + expect(parseIntervalAsMillisecond('20m')).toEqual(20 * 60 * 1000); + expect(parseIntervalAsMillisecond('61m')).toEqual(61 * 60 * 1000); + expect(parseIntervalAsMillisecond('90m')).toEqual(90 * 60 * 1000); + }); + }); + + describe('asInterval', () => { + test('returns a ms interval when ms duration can only divide by ms', () => { + expect(asInterval(500)).toEqual('500ms'); + expect(asInterval(1500)).toEqual('1500ms'); + expect(asInterval(1001)).toEqual('1001ms'); + expect(asInterval(2001)).toEqual('2001ms'); + expect(asInterval(61001)).toEqual('61001ms'); + expect(asInterval(90001)).toEqual('90001ms'); + }); + + test('returns a seconds interval when ms duration divides by seconds', () => { + expect(asInterval(1000)).toEqual('1s'); + expect(asInterval(2000)).toEqual('2s'); + expect(asInterval(61000)).toEqual('61s'); + expect(asInterval(99000)).toEqual('99s'); + expect(asInterval(90000)).toEqual('90s'); + }); + + test('returns a minutes interval when ms duration divides by minutes', () => { + expect(asInterval(60000)).toEqual('1m'); + expect(asInterval(120000)).toEqual('2m'); + }); }); describe('intervalFromNow', () => { @@ -125,25 +193,6 @@ describe('taskIntervals', () => { }); }); - describe('minutesFromNow', () => { - test('it returns the current date plus a number of minutes', () => { - const mins = _.random(1, 100); - const expected = Date.now() + mins * 60 * 1000; - const nextRun = minutesFromNow(mins).getTime(); - expect(nextRun).toEqual(expected); - }); - }); - - describe('minutesFromDate', () => { - test('it returns the given date plus a number of minutes', () => { - const originalDate = new Date(2019, 1, 1); - const mins = _.random(1, 100); - const expected = originalDate.valueOf() + mins * 60 * 1000; - const nextRun = minutesFromDate(originalDate, mins).getTime(); - expect(expected).toEqual(nextRun); - }); - }); - describe('secondsFromNow', () => { test('it returns the current date plus a number of seconds', () => { const secs = _.random(1, 100); diff --git a/x-pack/plugins/task_manager/server/lib/intervals.ts b/x-pack/plugins/task_manager/server/lib/intervals.ts index 9009be5f78220..a28dfa62a501f 100644 --- a/x-pack/plugins/task_manager/server/lib/intervals.ts +++ b/x-pack/plugins/task_manager/server/lib/intervals.ts @@ -4,6 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { memoize } from 'lodash'; + +export enum IntervalCadence { + Minute = 'm', + Second = 's', +} +const VALID_CADENCE = new Set(Object.values(IntervalCadence)); +const CADENCE_IN_MS: Record = { + [IntervalCadence.Second]: 1000, + [IntervalCadence.Minute]: 60 * 1000, +}; + +function isCadence(cadence: IntervalCadence | string): cadence is IntervalCadence { + return VALID_CADENCE.has(cadence as IntervalCadence); +} + +export function asInterval(ms: number): string { + const secondsRemainder = ms % 1000; + const minutesRemainder = ms % 60000; + return secondsRemainder ? `${ms}ms` : minutesRemainder ? `${ms / 1000}s` : `${ms / 60000}m`; +} + /** * Returns a date that is the specified interval from now. Currently, * only minute-intervals and second-intervals are supported. @@ -14,14 +36,7 @@ export function intervalFromNow(interval?: string): Date | undefined { if (interval === undefined) { return; } - - assertValidInterval(interval); - - if (isSeconds(interval)) { - return secondsFromNow(parseInterval(interval)); - } - - return minutesFromNow(parseInterval(interval)); + return secondsFromNow(parseIntervalAsSecond(interval)); } /** @@ -35,37 +50,7 @@ export function intervalFromDate(date: Date, interval?: string): Date | undefine if (interval === undefined) { return; } - - assertValidInterval(interval); - - if (isSeconds(interval)) { - return secondsFromDate(date, parseInterval(interval)); - } - - return minutesFromDate(date, parseInterval(interval)); -} - -/** - * Returns a date that is mins minutes from now. - * - * @param mins The number of mintues from now - */ -export function minutesFromNow(mins: number): Date { - return minutesFromDate(new Date(), mins); -} - -/** - * Returns a date that is mins minutes from given date. - * - * @param date The date to add minutes to - * @param mins The number of mintues from given date - */ -export function minutesFromDate(date: Date, mins: number): Date { - const result = new Date(date.valueOf()); - - result.setMinutes(result.getMinutes() + mins); - - return result; + return secondsFromDate(date, parseIntervalAsSecond(interval)); } /** @@ -85,9 +70,7 @@ export function secondsFromNow(secs: number): Date { */ export function secondsFromDate(date: Date, secs: number): Date { const result = new Date(date.valueOf()); - result.setSeconds(result.getSeconds() + secs); - return result; } @@ -95,29 +78,22 @@ export function secondsFromDate(date: Date, secs: number): Date { * Verifies that the specified interval matches our expected format. * * @param {string} interval - An interval such as `5m` or `10s` + * @returns {number} The interval as seconds */ -export function assertValidInterval(interval: string) { - if (isMinutes(interval)) { - return interval; +export const parseIntervalAsSecond = memoize((interval: string): number => { + return Math.round(parseIntervalAsMillisecond(interval) / 1000); +}); + +export const parseIntervalAsMillisecond = memoize((interval: string): number => { + const numericAsStr: string = interval.slice(0, -1); + const numeric: number = parseInt(numericAsStr, 10); + const cadence: IntervalCadence | string = interval.slice(-1); + if (!isCadence(cadence) || isNaN(numeric) || numeric <= 0 || !isNumeric(numericAsStr)) { + throw new Error( + `Invalid interval "${interval}". Intervals must be of the form {number}m. Example: 5m.` + ); } + return numeric * CADENCE_IN_MS[cadence]; +}); - if (isSeconds(interval)) { - return interval; - } - - throw new Error( - `Invalid interval "${interval}". Intervals must be of the form {number}m. Example: 5m.` - ); -} - -function parseInterval(interval: string) { - return parseInt(interval, 10); -} - -function isMinutes(interval: string) { - return /^[1-9][0-9]*m$/.test(interval); -} - -function isSeconds(interval: string) { - return /^[1-9][0-9]*s$/.test(interval); -} +const isNumeric = (numAsStr: string) => /^\d+$/.test(numAsStr); diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts new file mode 100644 index 0000000000000..f97861901b5b5 --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Subject } from 'rxjs'; +import { take, bufferCount } from 'rxjs/operators'; +import { createConfigurationAggregator } from './configuration_statistics'; +import { TaskManagerConfig } from '../config'; + +describe('Configuration Statistics Aggregator', () => { + test('merges the static config with the merged configs', async () => { + const configuration: TaskManagerConfig = { + enabled: true, + max_workers: 10, + index: 'foo', + max_attempts: 9, + poll_interval: 6000000, + monitored_stats_required_freshness: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + }; + + const managedConfig = { + maxWorkersConfiguration$: new Subject(), + pollIntervalConfiguration$: new Subject(), + }; + + return new Promise(async (resolve, reject) => { + createConfigurationAggregator(configuration, managedConfig) + .pipe(take(3), bufferCount(3)) + .subscribe(([initial, updatedWorkers, updatedInterval]) => { + expect(initial.value).toEqual({ + max_workers: 10, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + }); + + expect(updatedWorkers.value).toEqual({ + max_workers: 8, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + }); + + expect(updatedInterval.value).toEqual({ + max_workers: 8, + poll_interval: 3000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + }); + resolve(); + }, reject); + + managedConfig.maxWorkersConfiguration$.next(8); + + managedConfig.pollIntervalConfiguration$.next(3000); + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.ts new file mode 100644 index 0000000000000..22b08bc5c88db --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineLatest, of } from 'rxjs'; +import { pick, merge } from 'lodash'; +import { map, startWith } from 'rxjs/operators'; +import { AggregatedStatProvider } from './runtime_statistics_aggregator'; +import { TaskManagerConfig } from '../config'; +import { ManagedConfiguration } from '../lib/create_managed_configuration'; + +const CONFIG_FIELDS_TO_EXPOSE = [ + 'request_capacity', + 'max_poll_inactivity_cycles', + 'monitored_aggregated_stats_refresh_rate', + 'monitored_stats_running_average_window', + 'monitored_task_execution_thresholds', +] as const; + +export type ConfigStat = Pick< + TaskManagerConfig, + 'max_workers' | 'poll_interval' | typeof CONFIG_FIELDS_TO_EXPOSE[number] +>; + +export function createConfigurationAggregator( + config: TaskManagerConfig, + managedConfig: ManagedConfiguration +): AggregatedStatProvider { + return combineLatest([ + of(pick(config, ...CONFIG_FIELDS_TO_EXPOSE)), + managedConfig.pollIntervalConfiguration$.pipe( + startWith(config.poll_interval), + map>((pollInterval) => ({ + poll_interval: pollInterval, + })) + ), + managedConfig.maxWorkersConfiguration$.pipe( + startWith(config.max_workers), + map>((maxWorkers) => ({ + max_workers: maxWorkers, + })) + ), + ]).pipe( + map((configurations) => ({ + key: 'configuration', + value: merge({}, ...configurations), + })) + ); +} diff --git a/x-pack/plugins/task_manager/server/monitoring/index.ts b/x-pack/plugins/task_manager/server/monitoring/index.ts new file mode 100644 index 0000000000000..0a4c8c56a5a79 --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger } from 'src/core/server'; +import { Observable } from 'rxjs'; +import { TaskManagerConfig } from '../config'; +import { + MonitoringStats, + createAggregators, + createMonitoringStatsStream, +} from './monitoring_stats_stream'; +import { TaskStore } from '../task_store'; +import { TaskPollingLifecycle } from '../polling_lifecycle'; +import { ManagedConfiguration } from '../lib/create_managed_configuration'; + +export { + MonitoringStats, + HealthStatus, + RawMonitoringStats, + summarizeMonitoringStats, + createAggregators, + createMonitoringStatsStream, +} from './monitoring_stats_stream'; + +export function createMonitoringStats( + taskPollingLifecycle: TaskPollingLifecycle, + taskStore: TaskStore, + elasticsearchAndSOAvailability$: Observable, + config: TaskManagerConfig, + managedConfig: ManagedConfiguration, + logger: Logger +): Observable { + return createMonitoringStatsStream( + createAggregators( + taskPollingLifecycle, + taskStore, + elasticsearchAndSOAvailability$, + config, + managedConfig, + logger + ), + config + ); +} diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts new file mode 100644 index 0000000000000..8479def5deeeb --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TaskManagerConfig } from '../config'; +import { of, Subject } from 'rxjs'; +import { take, bufferCount } from 'rxjs/operators'; +import { createMonitoringStatsStream, AggregatedStat } from './monitoring_stats_stream'; +import { JsonValue } from 'src/plugins/kibana_utils/common'; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('createMonitoringStatsStream', () => { + const configuration: TaskManagerConfig = { + enabled: true, + max_workers: 10, + index: 'foo', + max_attempts: 9, + poll_interval: 6000000, + monitored_stats_required_freshness: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + }; + + it('returns the initial config used to configure Task Manager', async () => { + return new Promise((resolve) => { + createMonitoringStatsStream(of(), configuration) + .pipe(take(1)) + .subscribe((firstValue) => { + expect(firstValue.stats).toEqual({}); + resolve(); + }); + }); + }); + + it('incrementally updates the stats returned by the endpoint', async () => { + const aggregatedStats$ = new Subject(); + + return new Promise((resolve) => { + createMonitoringStatsStream(aggregatedStats$, configuration) + .pipe(take(3), bufferCount(3)) + .subscribe(([initialValue, secondValue, thirdValue]) => { + expect(initialValue.stats).toMatchObject({ + lastUpdate: expect.any(String), + stats: { + configuration: { + value: { + max_workers: 10, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + }, + }, + }, + }); + + expect(secondValue).toMatchObject({ + lastUpdate: expect.any(String), + stats: { + newAggregatedStat: { + timestamp: expect.any(String), + value: { + some: { + complex: { + value: 123, + }, + }, + }, + }, + configuration: { + timestamp: expect.any(String), + value: { + max_workers: 10, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + }, + }, + }, + }); + + expect(thirdValue).toMatchObject({ + lastUpdate: expect.any(String), + stats: { + newAggregatedStat: { + timestamp: expect.any(String), + value: { + some: { + updated: { + value: 456, + }, + }, + }, + }, + configuration: { + timestamp: expect.any(String), + value: { + max_workers: 10, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + }, + }, + }, + }); + }); + + aggregatedStats$.next({ + key: 'newAggregatedStat', + value: { + some: { + complex: { + value: 123, + }, + }, + } as JsonValue, + }); + + aggregatedStats$.next({ + key: 'newAggregatedStat', + value: { + some: { + updated: { + value: 456, + }, + }, + } as JsonValue, + }); + + resolve(); + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts new file mode 100644 index 0000000000000..524afb8d78e21 --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { merge, of, Observable } from 'rxjs'; +import { map, scan } from 'rxjs/operators'; +import { set } from '@elastic/safer-lodash-set'; +import { Logger } from 'src/core/server'; +import { JsonObject } from 'src/plugins/kibana_utils/common'; +import { TaskStore } from '../task_store'; +import { TaskPollingLifecycle } from '../polling_lifecycle'; +import { + createWorkloadAggregator, + summarizeWorkloadStat, + WorkloadStat, +} from './workload_statistics'; +import { + createTaskRunAggregator, + summarizeTaskRunStat, + TaskRunStat, + SummarizedTaskRunStat, +} from './task_run_statistics'; +import { ConfigStat, createConfigurationAggregator } from './configuration_statistics'; +import { TaskManagerConfig } from '../config'; +import { AggregatedStatProvider } from './runtime_statistics_aggregator'; +import { ManagedConfiguration } from '../lib/create_managed_configuration'; + +export { AggregatedStatProvider, AggregatedStat } from './runtime_statistics_aggregator'; + +export interface MonitoringStats { + last_update: string; + stats: { + configuration?: MonitoredStat; + workload?: MonitoredStat; + runtime?: MonitoredStat; + }; +} + +export enum HealthStatus { + OK = 'OK', + Warning = 'warn', + Error = 'error', +} + +interface MonitoredStat { + timestamp: string; + value: T; +} +type RawMonitoredStat = MonitoredStat & { + status: HealthStatus; +}; + +export interface RawMonitoringStats { + last_update: string; + stats: { + configuration?: RawMonitoredStat; + workload?: RawMonitoredStat; + runtime?: RawMonitoredStat; + }; +} + +export function createAggregators( + taskPollingLifecycle: TaskPollingLifecycle, + taskStore: TaskStore, + elasticsearchAndSOAvailability$: Observable, + config: TaskManagerConfig, + managedConfig: ManagedConfiguration, + logger: Logger +): AggregatedStatProvider { + return merge( + createConfigurationAggregator(config, managedConfig), + createTaskRunAggregator(taskPollingLifecycle, config.monitored_stats_running_average_window), + createWorkloadAggregator( + taskStore, + elasticsearchAndSOAvailability$, + config.monitored_aggregated_stats_refresh_rate, + config.poll_interval, + logger + ) + ); +} + +export function createMonitoringStatsStream( + provider$: AggregatedStatProvider, + config: TaskManagerConfig +): Observable { + const initialStats = { + last_update: new Date().toISOString(), + stats: {}, + }; + return merge( + // emit the initial stats + of(initialStats), + // emit updated stats whenever a provider updates a specific key on the stats + provider$.pipe( + map(({ key, value }) => { + return { + value: { timestamp: new Date().toISOString(), value }, + key, + }; + }), + scan((monitoringStats: MonitoringStats, { key, value }) => { + // incrementally merge stats as they come in + set(monitoringStats.stats, key, value); + monitoringStats.last_update = new Date().toISOString(); + return monitoringStats; + }, initialStats) + ) + ); +} + +export function summarizeMonitoringStats( + { + // eslint-disable-next-line @typescript-eslint/naming-convention + last_update, + stats: { runtime, workload, configuration }, + }: MonitoringStats, + config: TaskManagerConfig +): RawMonitoringStats { + return { + last_update, + stats: { + ...(configuration + ? { + configuration: { + ...configuration, + status: HealthStatus.OK, + }, + } + : {}), + ...(runtime + ? { + runtime: { + timestamp: runtime.timestamp, + ...summarizeTaskRunStat(runtime.value, config), + }, + } + : {}), + ...(workload + ? { + workload: { + timestamp: workload.timestamp, + ...summarizeWorkloadStat(workload.value), + }, + } + : {}), + }, + }; +} diff --git a/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts b/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts new file mode 100644 index 0000000000000..bd2b3845f2526 --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Observable } from 'rxjs'; +import { JsonValue } from 'src/plugins/kibana_utils/common'; + +export interface AggregatedStat { + key: string; + value: Stat; +} + +export type AggregatedStatProvider = Observable< + AggregatedStat +>; diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.test.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.test.ts new file mode 100644 index 0000000000000..eb8cabd9f3a8f --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; + +import { + calculateRunningAverage, + calculateFrequency, + createRunningAveragedStat, + createMapOfRunningAveragedStats, +} from './task_run_calcultors'; + +describe('calculateRunningAverage', () => { + test('calculates the running average and median of a window of values', async () => { + expect(calculateRunningAverage([2, 2, 4, 6, 6])).toMatchInlineSnapshot(` + Object { + "p50": 4, + "p90": 6, + "p95": 6, + "p99": 6, + } + `); + }); +}); + +describe('calculateFrequency', () => { + test('calculates the frequency of each terms in the list as a percentage', async () => { + const [term1, term2, term3] = [uuid.v4(), uuid.v4(), uuid.v4()]; + expect( + calculateFrequency([term1, term2, term2, term3, term1, term1, term2, term1, term3]) + ).toEqual({ + [term3]: 22, + [term1]: 44, + [term2]: 33, + }); + }); +}); + +describe('createRunningAveragedStat', () => { + test('create a function which tracks a window of values', async () => { + const queue = createRunningAveragedStat(3); + expect(queue(1)).toEqual([1]); + expect(queue(2)).toEqual([1, 2]); + expect(queue(3)).toEqual([1, 2, 3]); + expect(queue(4)).toEqual([2, 3, 4]); + expect(queue(5)).toEqual([3, 4, 5]); + }); +}); + +describe('createMapOfRunningAveragedStats', () => { + test('create a function which tracks multiple window of values by key', async () => { + const [term1, term2, term3] = [uuid.v4(), uuid.v4(), uuid.v4()]; + const mappedQueues = createMapOfRunningAveragedStats(3); + expect(mappedQueues(term1, 1)).toEqual({ [term1]: [1] }); + expect(mappedQueues(term1, 2)).toEqual({ [term1]: [1, 2] }); + expect(mappedQueues(term2, 3)).toEqual({ [term1]: [1, 2], [term2]: [3] }); + expect(mappedQueues(term3, 4)).toEqual({ [term1]: [1, 2], [term2]: [3], [term3]: [4] }); + expect(mappedQueues(term2, 5)).toEqual({ [term1]: [1, 2], [term2]: [3, 5], [term3]: [4] }); + expect(mappedQueues(term2, 6)).toEqual({ [term1]: [1, 2], [term2]: [3, 5, 6], [term3]: [4] }); + expect(mappedQueues(term1, 7)).toEqual({ + [term1]: [1, 2, 7], + [term2]: [3, 5, 6], + [term3]: [4], + }); + expect(mappedQueues(term1, 8)).toEqual({ + [term1]: [2, 7, 8], + [term2]: [3, 5, 6], + [term3]: [4], + }); + expect(mappedQueues(term1, 9)).toEqual({ + [term1]: [7, 8, 9], + [term2]: [3, 5, 6], + [term3]: [4], + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts new file mode 100644 index 0000000000000..67b77a29b1c76 --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import stats from 'stats-lite'; +import { JsonObject } from 'src/plugins/kibana_utils/common'; +import { isUndefined, countBy, mapValues } from 'lodash'; + +export interface AveragedStat extends JsonObject { + p50: number; + p90: number; + p95: number; + p99: number; +} + +export function calculateRunningAverage(values: number[]): AveragedStat { + return { + p50: stats.percentile(values, 0.5), + p90: stats.percentile(values, 0.9), + p95: stats.percentile(values, 0.95), + p99: stats.percentile(values, 0.99), + }; +} + +/** + * Calculate the frequency of each term in a list of terms. + * @param values + */ +export function calculateFrequency(values: T[]): JsonObject { + return values.length + ? mapValues(countBy(values), (count) => Math.round((count * 100) / values.length)) + : {}; +} + +/** + * Utility to keep track of a bounded array of values which changes over time + * dropping older values as they slide out of the window we wish to track + */ +export function createRunningAveragedStat(runningAverageWindowSize: number) { + const list = new Array(); + return (value?: T) => { + if (!isUndefined(value)) { + if (list.length === runningAverageWindowSize) { + list.shift(); + } + list.push(value); + } + // clone list to ensure it isn't mutated externally + return [...list]; + }; +} + +export function createMapOfRunningAveragedStats(runningAverageWindowSize: number) { + const mappedQueue: Record T[]> = {}; + const asRecordOfValues = () => mapValues(mappedQueue, (queue) => queue()); + return (key?: string, value?: T) => { + if (!isUndefined(key)) { + mappedQueue[key] = mappedQueue[key] ?? createRunningAveragedStat(runningAverageWindowSize); + mappedQueue[key](value); + } + return asRecordOfValues(); + }; +} diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts new file mode 100644 index 0000000000000..a931f0ff7c304 --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.test.ts @@ -0,0 +1,501 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { Subject, Observable } from 'rxjs'; +import stats from 'stats-lite'; +import sinon from 'sinon'; +import { take, tap, bufferCount, skip, map } from 'rxjs/operators'; + +import { ConcreteTaskInstance, TaskStatus } from '../task'; +import { asTaskRunEvent, asTaskPollingCycleEvent, TaskTiming } from '../task_events'; +import { asOk } from '../lib/result_type'; +import { TaskLifecycleEvent } from '../polling_lifecycle'; +import { TaskRunResult } from '../task_runner'; +import { + createTaskRunAggregator, + summarizeTaskRunStat, + TaskRunStat, + SummarizedTaskRunStat, +} from './task_run_statistics'; +import { AggregatedStat } from './runtime_statistics_aggregator'; +import { FillPoolResult } from '../lib/fill_pool'; +import { taskPollingLifecycleMock } from '../polling_lifecycle.mock'; +import { configSchema } from '../config'; + +describe('Task Run Statistics', () => { + let fakeTimer: sinon.SinonFakeTimers; + + beforeAll(() => { + fakeTimer = sinon.useFakeTimers(); + }); + + afterAll(() => fakeTimer.restore()); + + test('returns a running average of task drift', async () => { + const runAtDrift = [1000, 2000, 500, 300, 400, 15000, 20000, 200]; + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const runningAverageWindowSize = 5; + const taskRunAggregator = createTaskRunAggregator( + taskPollingLifecycle, + runningAverageWindowSize + ); + + function expectWindowEqualsUpdate( + taskStat: AggregatedStat, + window: number[] + ) { + expect(taskStat.value.drift).toMatchObject({ + p50: stats.percentile(window, 0.5), + p90: stats.percentile(window, 0.9), + p95: stats.percentile(window, 0.95), + p99: stats.percentile(window, 0.99), + }); + } + + return new Promise((resolve) => { + taskRunAggregator + .pipe( + // skip initial stat which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + // Use 'summarizeTaskRunStat' to receive summarize stats + map(({ key, value }: AggregatedStat) => ({ + key, + value: summarizeTaskRunStat(value, getTaskManagerConfig()).value, + })), + take(runAtDrift.length), + bufferCount(runAtDrift.length) + ) + .subscribe((taskStats: Array>) => { + expectWindowEqualsUpdate(taskStats[0], runAtDrift.slice(0, 1)); + expectWindowEqualsUpdate(taskStats[1], runAtDrift.slice(0, 2)); + expectWindowEqualsUpdate(taskStats[2], runAtDrift.slice(0, 3)); + expectWindowEqualsUpdate(taskStats[3], runAtDrift.slice(0, 4)); + expectWindowEqualsUpdate(taskStats[4], runAtDrift.slice(0, 5)); + // from the 6th value, begin to drop old values as out window is 5 + expectWindowEqualsUpdate(taskStats[5], runAtDrift.slice(1, 6)); + expectWindowEqualsUpdate(taskStats[6], runAtDrift.slice(2, 7)); + expectWindowEqualsUpdate(taskStats[7], runAtDrift.slice(3, 8)); + resolve(); + }); + + const now = Date.now(); + for (const drift of runAtDrift) { + const start = Math.floor(Math.random() * 1000); + events$.next( + mockTaskRunEvent( + { runAt: runAtMillisecondsAgo(drift + start) }, + { start: runAtMillisecondsAgo(start).getTime(), stop: now } + ) + ); + } + }); + }); + + test('returns a running average of task run duration', async () => { + const runDurations = [1000, 2000, 500, 300, 400, 15000, 20000, 200]; + const runDurationsInReverse = runDurations.reverse(); + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const runningAverageWindowSize = 5; + const taskRunAggregator = createTaskRunAggregator( + taskPollingLifecycle, + runningAverageWindowSize + ); + + function expectWindowEqualsUpdate( + taskStat: AggregatedStat, + windows: Record + ) { + for (const [type, window] of Object.entries(windows)) { + expect(taskStat.value.execution.duration[type]).toMatchObject({ + p50: stats.percentile(window, 0.5), + p90: stats.percentile(window, 0.9), + p95: stats.percentile(window, 0.95), + p99: stats.percentile(window, 0.99), + }); + } + } + + return new Promise((resolve, reject) => { + taskRunAggregator + .pipe( + // skip initial stat which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + // Use 'summarizeTaskRunStat' to receive summarize stats + map(({ key, value }: AggregatedStat) => ({ + key, + value: summarizeTaskRunStat(value, getTaskManagerConfig()).value, + })), + take(runDurations.length * 2), + bufferCount(runDurations.length * 2) + ) + .subscribe((taskStats: Array>) => { + try { + expectWindowEqualsUpdate(taskStats[0], { 'alerting:test': runDurations.slice(0, 1) }); + expectWindowEqualsUpdate(taskStats[1], { 'alerting:test': runDurations.slice(0, 2) }); + expectWindowEqualsUpdate(taskStats[2], { 'alerting:test': runDurations.slice(0, 3) }); + expectWindowEqualsUpdate(taskStats[3], { 'alerting:test': runDurations.slice(0, 4) }); + expectWindowEqualsUpdate(taskStats[4], { 'alerting:test': runDurations.slice(0, 5) }); + // from the 6th value, begin to drop old values as out window is 5 + expectWindowEqualsUpdate(taskStats[5], { 'alerting:test': runDurations.slice(1, 6) }); + expectWindowEqualsUpdate(taskStats[6], { 'alerting:test': runDurations.slice(2, 7) }); + expectWindowEqualsUpdate(taskStats[7], { 'alerting:test': runDurations.slice(3, 8) }); + expectWindowEqualsUpdate(taskStats[8], { + 'actions:test': runDurations.slice(0, 1), + 'alerting:test': runDurations.slice(3, 8), + }); + expectWindowEqualsUpdate(taskStats[9], { + 'actions:test': runDurations.slice(0, 2), + 'alerting:test': runDurations.slice(3, 8), + }); + expectWindowEqualsUpdate(taskStats[10], { + 'actions:test': runDurations.slice(0, 3), + 'alerting:test': runDurations.slice(3, 8), + }); + expectWindowEqualsUpdate(taskStats[11], { + 'actions:test': runDurations.slice(0, 4), + 'alerting:test': runDurations.slice(3, 8), + }); + expectWindowEqualsUpdate(taskStats[12], { + 'actions:test': runDurations.slice(0, 5), + 'alerting:test': runDurations.slice(3, 8), + }); + // from the 6th value, begin to drop old values as out window is 5 + expectWindowEqualsUpdate(taskStats[13], { + 'actions:test': runDurations.slice(1, 6), + 'alerting:test': runDurations.slice(3, 8), + }); + expectWindowEqualsUpdate(taskStats[14], { + 'actions:test': runDurations.slice(2, 7), + 'alerting:test': runDurations.slice(3, 8), + }); + expectWindowEqualsUpdate(taskStats[15], { + 'actions:test': runDurations.slice(3, 8), + 'alerting:test': runDurations.slice(3, 8), + }); + resolve(); + } catch (e) { + reject(e); + } + }); + + const now = Date.now(); + for (const runDuration of runDurations) { + events$.next( + mockTaskRunEvent( + { taskType: 'alerting:test' }, + { start: runAtMillisecondsAgo(runDuration).getTime(), stop: now } + ) + ); + } + for (const runDuration of runDurationsInReverse) { + events$.next( + mockTaskRunEvent( + { taskType: 'actions:test' }, + { start: runAtMillisecondsAgo(runDuration).getTime(), stop: now } + ) + ); + } + }); + }); + + test('returns the frequency of task run results', async () => { + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const runningAverageWindowSize = 5; + const taskRunAggregator = createTaskRunAggregator( + taskPollingLifecycle, + runningAverageWindowSize + ); + + return new Promise((resolve, reject) => { + taskRunAggregator + .pipe( + // skip initial stat which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + // Use 'summarizeTaskRunStat' to receive summarize stats + map(({ key, value }: AggregatedStat) => ({ + key, + value: summarizeTaskRunStat(value, getTaskManagerConfig()).value, + })), + take(10), + bufferCount(10) + ) + .subscribe((taskStats: Array>) => { + try { + /** + * At any given time we only keep track of the last X Polling Results + * In the tests this is ocnfiugured to a window size of 5 + */ + expect( + taskStats.map( + (taskStat) => + taskStat.value.execution.result_frequency_percent_as_number['alerting:test'] + ) + ).toEqual([ + // Success + { Success: 100, RetryScheduled: 0, Failed: 0, status: 'OK' }, + // Success, Success, + { Success: 100, RetryScheduled: 0, Failed: 0, status: 'OK' }, + // Success, Success, Success + { Success: 100, RetryScheduled: 0, Failed: 0, status: 'OK' }, + // Success, Success, Success, Failed + { Success: 75, RetryScheduled: 0, Failed: 25, status: 'OK' }, + // Success, Success, Success, Failed, Failed + { Success: 60, RetryScheduled: 0, Failed: 40, status: 'OK' }, + // Success, Success, Failed, Failed, Failed + { Success: 40, RetryScheduled: 0, Failed: 60, status: 'OK' }, + // Success, Failed, Failed, Failed, RetryScheduled + { Success: 20, RetryScheduled: 20, Failed: 60, status: 'OK' }, + // Failed, Failed, Failed, RetryScheduled, RetryScheduled + { Success: 0, RetryScheduled: 40, Failed: 60, status: 'OK' }, + // Failed, Failed, RetryScheduled, RetryScheduled, Success + { Success: 20, RetryScheduled: 40, Failed: 40, status: 'OK' }, + // Failed, RetryScheduled, RetryScheduled, Success, Success + { Success: 40, RetryScheduled: 40, Failed: 20, status: 'OK' }, + ]); + resolve(); + } catch (e) { + reject(e); + } + }); + + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Failed)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Failed)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Failed)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.RetryScheduled)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.RetryScheduled)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + }); + }); + + test('frequency of task run results return an error health status when failure is above a certain threshold', async () => { + const events$ = new Subject(); + + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const runningAverageWindowSize = 5; + const taskRunAggregator = createTaskRunAggregator( + taskPollingLifecycle, + runningAverageWindowSize + ); + + return new Promise((resolve, reject) => { + taskRunAggregator + .pipe( + // skip initial stat which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + // Use 'summarizeTaskRunStat' to receive summarize stats + map(({ key, value }: AggregatedStat) => ({ + key, + value: summarizeTaskRunStat( + value, + getTaskManagerConfig({ + monitored_task_execution_thresholds: { + custom: { + 'alerting:test': { + error_threshold: 59, + warn_threshold: 39, + }, + }, + }, + }) + ).value, + })), + take(10), + bufferCount(10) + ) + .subscribe((taskStats: Array>) => { + try { + /** + * At any given time we only keep track of the last X Polling Results + * In the tests this is ocnfiugured to a window size of 5 + */ + expect( + taskStats.map( + (taskStat) => + taskStat.value.execution.result_frequency_percent_as_number['alerting:test'] + ) + ).toEqual([ + // Success + { Success: 100, RetryScheduled: 0, Failed: 0, status: 'OK' }, + // Success, Success, + { Success: 100, RetryScheduled: 0, Failed: 0, status: 'OK' }, + // Success, Success, Success + { Success: 100, RetryScheduled: 0, Failed: 0, status: 'OK' }, + // Success, Success, Success, Failed + { Success: 75, RetryScheduled: 0, Failed: 25, status: 'OK' }, + // Success, Success, Success, Failed, Failed + { Success: 60, RetryScheduled: 0, Failed: 40, status: 'warn' }, + // Success, Success, Failed, Failed, Failed + { Success: 40, RetryScheduled: 0, Failed: 60, status: 'error' }, + // Success, Failed, Failed, Failed, RetryScheduled + { Success: 20, RetryScheduled: 20, Failed: 60, status: 'error' }, + // Failed, Failed, Failed, RetryScheduled, RetryScheduled + { Success: 0, RetryScheduled: 40, Failed: 60, status: 'error' }, + // Failed, Failed, RetryScheduled, RetryScheduled, Success + { Success: 20, RetryScheduled: 40, Failed: 40, status: 'warn' }, + // Failed, RetryScheduled, RetryScheduled, Success, Success + { Success: 40, RetryScheduled: 40, Failed: 20, status: 'OK' }, + ]); + resolve(); + } catch (e) { + reject(e); + } + }); + + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Failed)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Failed)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Failed)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.RetryScheduled)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.RetryScheduled)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + events$.next(mockTaskRunEvent({}, { start: 0, stop: 0 }, TaskRunResult.Success)); + }); + }); + + test('returns polling stats', async () => { + const expectedTimestamp: string[] = []; + const events$ = new Subject(); + const taskPollingLifecycle = taskPollingLifecycleMock.create({ + events$: events$ as Observable, + }); + + const runningAverageWindowSize = 5; + const taskRunAggregator = createTaskRunAggregator( + taskPollingLifecycle, + runningAverageWindowSize + ); + + return new Promise((resolve, reject) => { + taskRunAggregator + .pipe( + // skip initial stat which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + // Use 'summarizeTaskRunStat' to receive summarize stats + map(({ key, value }: AggregatedStat) => ({ + key, + value: summarizeTaskRunStat(value, getTaskManagerConfig()).value, + })), + tap(() => { + expectedTimestamp.push(new Date().toISOString()); + // each event is a second after the previous one + fakeTimer.tick(1000); + }), + take(10), + bufferCount(10) + ) + .subscribe((taskStats: Array>) => { + try { + expect( + taskStats.map((taskStat) => taskStat.value.polling.last_successful_poll) + ).toEqual(expectedTimestamp); + + /** + * At any given time we only keep track of the last X Polling Results + * In the tests this is ocnfiugured to a window size of 5 + */ + expect( + taskStats.map((taskStat) => taskStat.value.polling.result_frequency_percent_as_number) + ).toEqual([ + // NoTasksClaimed + { NoTasksClaimed: 100, RanOutOfCapacity: 0, PoolFilled: 0 }, + // NoTasksClaimed, NoTasksClaimed, + { NoTasksClaimed: 100, RanOutOfCapacity: 0, PoolFilled: 0 }, + // NoTasksClaimed, NoTasksClaimed, NoTasksClaimed + { NoTasksClaimed: 100, RanOutOfCapacity: 0, PoolFilled: 0 }, + // NoTasksClaimed, NoTasksClaimed, NoTasksClaimed, PoolFilled + { NoTasksClaimed: 75, RanOutOfCapacity: 0, PoolFilled: 25 }, + // NoTasksClaimed, NoTasksClaimed, NoTasksClaimed, PoolFilled, PoolFilled + { NoTasksClaimed: 60, RanOutOfCapacity: 0, PoolFilled: 40 }, + // NoTasksClaimed, NoTasksClaimed, PoolFilled, PoolFilled, PoolFilled + { NoTasksClaimed: 40, RanOutOfCapacity: 0, PoolFilled: 60 }, + // NoTasksClaimed, PoolFilled, PoolFilled, PoolFilled, RanOutOfCapacity + { NoTasksClaimed: 20, RanOutOfCapacity: 20, PoolFilled: 60 }, + // PoolFilled, PoolFilled, PoolFilled, RanOutOfCapacity, RanOutOfCapacity + { NoTasksClaimed: 0, RanOutOfCapacity: 40, PoolFilled: 60 }, + // PoolFilled, PoolFilled, RanOutOfCapacity, RanOutOfCapacity, NoTasksClaimed + { NoTasksClaimed: 20, RanOutOfCapacity: 40, PoolFilled: 40 }, + // PoolFilled, RanOutOfCapacity, RanOutOfCapacity, NoTasksClaimed, NoTasksClaimed + { NoTasksClaimed: 40, RanOutOfCapacity: 40, PoolFilled: 20 }, + ]); + resolve(); + } catch (e) { + reject(e); + } + }); + + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.NoTasksClaimed))); + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.NoTasksClaimed))); + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.NoTasksClaimed))); + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.PoolFilled))); + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.PoolFilled))); + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.PoolFilled))); + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.RanOutOfCapacity))); + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.RanOutOfCapacity))); + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.NoTasksClaimed))); + events$.next(asTaskPollingCycleEvent(asOk(FillPoolResult.NoTasksClaimed))); + }); + }); +}); + +function runAtMillisecondsAgo(ms: number): Date { + return new Date(Date.now() - ms); +} + +const mockTaskRunEvent = ( + overrides: Partial = {}, + timing: TaskTiming, + result: TaskRunResult = TaskRunResult.Success +) => { + const task = mockTaskInstance(overrides); + return asTaskRunEvent(task.id, asOk({ task, result }), timing); +}; + +const mockTaskInstance = (overrides: Partial = {}): ConcreteTaskInstance => ({ + id: uuid.v4(), + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + ...overrides, +}); + +const getTaskManagerConfig = (overrides: unknown = {}) => configSchema.validate(overrides); diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts new file mode 100644 index 0000000000000..6dd533177a86e --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineLatest, Observable } from 'rxjs'; +import { filter, startWith, map } from 'rxjs/operators'; +import { JsonObject } from 'src/plugins/kibana_utils/common'; +import { mapValues } from 'lodash'; +import { AggregatedStatProvider, AggregatedStat } from './runtime_statistics_aggregator'; +import { TaskLifecycleEvent } from '../polling_lifecycle'; +import { + isTaskRunEvent, + isTaskPollingCycleEvent, + TaskRun, + ErroredTask, + RanTask, + TaskTiming, +} from '../task_events'; +import { isOk, Ok, unwrap } from '../lib/result_type'; +import { ConcreteTaskInstance } from '../task'; +import { TaskRunResult } from '../task_runner'; +import { FillPoolResult } from '../lib/fill_pool'; +import { + AveragedStat, + calculateRunningAverage, + calculateFrequency, + createRunningAveragedStat, + createMapOfRunningAveragedStats, +} from './task_run_calcultors'; +import { HealthStatus } from './monitoring_stats_stream'; +import { TaskPollingLifecycle } from '../polling_lifecycle'; +import { TaskExecutionFailureThreshold, TaskManagerConfig } from '../config'; + +interface FillPoolStat extends JsonObject { + last_successful_poll: string; + result_frequency_percent_as_number: FillPoolResult[]; +} + +interface ExecutionStat extends JsonObject { + duration: Record; + result_frequency_percent_as_number: Record; +} + +export interface TaskRunStat extends JsonObject { + drift: number[]; + execution: ExecutionStat; + polling: FillPoolStat | Omit; +} + +interface FillPoolRawStat extends JsonObject { + last_successful_poll: string; + result_frequency_percent_as_number: { + [FillPoolResult.NoTasksClaimed]: number; + [FillPoolResult.RanOutOfCapacity]: number; + [FillPoolResult.PoolFilled]: number; + }; +} + +interface ResultFrequency extends JsonObject { + [TaskRunResult.Success]: number; + [TaskRunResult.SuccessRescheduled]: number; + [TaskRunResult.RetryScheduled]: number; + [TaskRunResult.Failed]: number; +} + +type ResultFrequencySummary = ResultFrequency & { + status: HealthStatus; +}; + +export interface SummarizedTaskRunStat extends JsonObject { + drift: AveragedStat; + execution: { + duration: Record; + result_frequency_percent_as_number: Record; + }; + polling: FillPoolRawStat | Omit; +} + +export function createTaskRunAggregator( + taskPollingLifecycle: TaskPollingLifecycle, + runningAverageWindowSize: number +): AggregatedStatProvider { + const taskRunEventToStat = createTaskRunEventToStat(runningAverageWindowSize); + const taskRunEvents$: Observable> = taskPollingLifecycle.events.pipe( + filter((taskEvent: TaskLifecycleEvent) => isTaskRunEvent(taskEvent) && hasTiming(taskEvent)), + map((taskEvent: TaskLifecycleEvent) => { + const { task, result }: RanTask | ErroredTask = unwrap((taskEvent as TaskRun).event); + return taskRunEventToStat(task, taskEvent.timing!, result); + }) + ); + + const resultFrequencyQueue = createRunningAveragedStat(runningAverageWindowSize); + const taskPollingEvents$: Observable> = taskPollingLifecycle.events.pipe( + filter( + (taskEvent: TaskLifecycleEvent) => + isTaskPollingCycleEvent(taskEvent) && isOk(taskEvent.event) + ), + map((taskEvent: TaskLifecycleEvent) => { + return { + polling: { + last_successful_poll: new Date().toISOString(), + result_frequency_percent_as_number: resultFrequencyQueue( + ((taskEvent.event as unknown) as Ok).value + ), + }, + }; + }) + ); + + return combineLatest([ + taskRunEvents$.pipe( + startWith({ drift: [], execution: { duration: {}, result_frequency_percent_as_number: {} } }) + ), + taskPollingEvents$.pipe( + startWith({ + polling: { result_frequency_percent_as_number: [] }, + }) + ), + ]).pipe( + map(([taskRun, polling]: [Omit, Pick]) => { + return { + key: 'runtime', + value: { + ...taskRun, + ...polling, + }, + } as AggregatedStat; + }) + ); +} + +function hasTiming(taskEvent: TaskLifecycleEvent) { + return !!taskEvent?.timing; +} + +function createTaskRunEventToStat(runningAverageWindowSize: number) { + const driftQueue = createRunningAveragedStat(runningAverageWindowSize); + const taskRunDurationQueue = createMapOfRunningAveragedStats(runningAverageWindowSize); + const resultFrequencyQueue = createMapOfRunningAveragedStats( + runningAverageWindowSize + ); + return ( + task: ConcreteTaskInstance, + timing: TaskTiming, + result: TaskRunResult + ): Omit => ({ + drift: driftQueue(timing!.start - task.runAt.getTime()), + execution: { + duration: taskRunDurationQueue(task.taskType, timing!.stop - timing!.start), + result_frequency_percent_as_number: resultFrequencyQueue(task.taskType, result), + }, + }); +} + +const DEFAULT_TASK_RUN_FREQUENCIES = { + [TaskRunResult.Success]: 0, + [TaskRunResult.SuccessRescheduled]: 0, + [TaskRunResult.RetryScheduled]: 0, + [TaskRunResult.Failed]: 0, +}; +const DEFAULT_POLLING_FREQUENCIES = { + [FillPoolResult.NoTasksClaimed]: 0, + [FillPoolResult.RanOutOfCapacity]: 0, + [FillPoolResult.PoolFilled]: 0, +}; + +export function summarizeTaskRunStat( + { + // eslint-disable-next-line @typescript-eslint/naming-convention + polling: { last_successful_poll, result_frequency_percent_as_number: pollingResultFrequency }, + drift, + execution: { duration, result_frequency_percent_as_number: executionResultFrequency }, + }: TaskRunStat, + config: TaskManagerConfig +): { value: SummarizedTaskRunStat; status: HealthStatus } { + return { + value: { + polling: { + ...(last_successful_poll ? { last_successful_poll } : {}), + result_frequency_percent_as_number: { + ...DEFAULT_POLLING_FREQUENCIES, + ...calculateFrequency(pollingResultFrequency as FillPoolResult[]), + }, + }, + drift: calculateRunningAverage(drift), + execution: { + duration: mapValues(duration, (typedDurations) => calculateRunningAverage(typedDurations)), + result_frequency_percent_as_number: mapValues( + executionResultFrequency, + (typedResultFrequencies, taskType) => + summarizeTaskExecutionResultFrequencyStat( + { + ...DEFAULT_TASK_RUN_FREQUENCIES, + ...calculateFrequency(typedResultFrequencies), + }, + config.monitored_task_execution_thresholds.custom[taskType] ?? + config.monitored_task_execution_thresholds.default + ) + ), + }, + }, + status: HealthStatus.OK, + }; +} + +function summarizeTaskExecutionResultFrequencyStat( + resultFrequencySummary: ResultFrequency, + executionErrorThreshold: TaskExecutionFailureThreshold +): ResultFrequencySummary { + return { + ...resultFrequencySummary, + status: + resultFrequencySummary.Failed > executionErrorThreshold.warn_threshold + ? resultFrequencySummary.Failed > executionErrorThreshold.error_threshold + ? HealthStatus.Error + : HealthStatus.Warning + : HealthStatus.OK, + }; +} diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts new file mode 100644 index 0000000000000..cb6e48530b027 --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -0,0 +1,819 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { first, take, bufferCount } from 'rxjs/operators'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { + WorkloadAggregation, + createWorkloadAggregator, + padBuckets, + estimateRecurringTaskScheduling, +} from './workload_statistics'; +import { ConcreteTaskInstance } from '../task'; +import { ESSearchResponse } from '../../../apm/typings/elasticsearch'; +import { AggregationResultOf } from '../../../apm/typings/elasticsearch/aggregations'; +import { times } from 'lodash'; +import { taskStoreMock } from '../task_store.mock'; +import { of, Subject } from 'rxjs'; +import { sleep } from '../test_utils'; + +type MockESResult = ESSearchResponse< + ConcreteTaskInstance, + { + body: WorkloadAggregation; + } +>; + +describe('Workload Statistics Aggregator', () => { + test('queries the Task Store at a fixed interval for the current workload', async () => { + const taskStore = taskStoreMock.create({}); + taskStore.aggregate.mockResolvedValue({ + hits: { + hits: [], + max_score: 0, + total: { value: 0, relation: 'eq' }, + }, + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 1, + failed: 0, + }, + aggregations: { + taskType: { + buckets: [], + }, + schedule: { + buckets: [], + }, + idleTasks: { + doc_count: 0, + overdue: { + doc_count: 0, + }, + scheduleDensity: { + buckets: [ + { + key: '2020-10-02T15:18:37.274Z-2020-10-02T15:19:36.274Z', + from: 1.601651917274e12, + from_as_string: '2020-10-02T15:18:37.274Z', + to: 1.601651976274e12, + to_as_string: '2020-10-02T15:19:36.274Z', + doc_count: 0, + histogram: { + buckets: [], + }, + }, + ], + }, + }, + }, + } as MockESResult); + + const workloadAggregator = createWorkloadAggregator( + taskStore, + of(true), + 10, + 3000, + loggingSystemMock.create().get() + ); + + return new Promise((resolve) => { + workloadAggregator.pipe(first()).subscribe(() => { + expect(taskStore.aggregate).toHaveBeenCalledWith({ + aggs: { + taskType: { + terms: { field: 'task.taskType' }, + aggs: { + status: { + terms: { field: 'task.status' }, + }, + }, + }, + schedule: { + terms: { + field: 'task.schedule.interval', + }, + }, + idleTasks: { + filter: { + term: { 'task.status': 'idle' }, + }, + aggs: { + scheduleDensity: { + range: { + field: 'task.runAt', + ranges: [{ from: 'now', to: 'now+1m' }], + }, + aggs: { + histogram: { + date_histogram: { + field: 'task.runAt', + fixed_interval: '3s', + }, + aggs: { + interval: { + terms: { + field: 'task.schedule.interval', + }, + }, + }, + }, + }, + }, + overdue: { + filter: { + range: { + 'task.runAt': { lt: 'now' }, + }, + }, + }, + }, + }, + }, + }); + resolve(); + }); + }); + }); + + const mockAggregatedResult: () => MockESResult = () => + ({ + hits: { + hits: [], + max_score: 0, + total: { value: 4, relation: 'eq' }, + }, + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 1, + failed: 0, + }, + aggregations: { + schedule: { + buckets: [ + { + key: '3600s', + doc_count: 1, + }, + { + key: '60s', + doc_count: 1, + }, + { + key: '720m', + doc_count: 1, + }, + ], + }, + taskType: { + buckets: [ + { + key: 'actions_telemetry', + doc_count: 2, + status: { + buckets: [ + { + key: 'idle', + doc_count: 2, + }, + ], + }, + }, + { + key: 'alerting_telemetry', + doc_count: 1, + status: { + buckets: [ + { + key: 'idle', + doc_count: 1, + }, + ], + }, + }, + { + key: 'session_cleanup', + doc_count: 1, + status: { + buckets: [ + { + key: 'idle', + doc_count: 1, + }, + ], + }, + }, + ], + }, + idleTasks: { + doc_count: 13, + overdue: { + doc_count: 6, + }, + scheduleDensity: { + buckets: [ + mockHistogram(0, 7 * 3000 + 500, 60 * 1000, 3000, [2, 2, 5, 0, 0, 0, 0, 0, 0, 1]), + ], + }, + }, + }, + } as MockESResult); + + test('returns a summary of the workload by task type', async () => { + const taskStore = taskStoreMock.create({}); + taskStore.aggregate.mockResolvedValue(mockAggregatedResult()); + + const workloadAggregator = createWorkloadAggregator( + taskStore, + of(true), + 10, + 3000, + loggingSystemMock.create().get() + ); + + return new Promise((resolve) => { + workloadAggregator.pipe(first()).subscribe((result) => { + expect(result.key).toEqual('workload'); + expect(result.value).toMatchObject({ + count: 4, + task_types: { + actions_telemetry: { count: 2, status: { idle: 2 } }, + alerting_telemetry: { count: 1, status: { idle: 1 } }, + session_cleanup: { count: 1, status: { idle: 1 } }, + }, + }); + resolve(); + }); + }); + }); + + test('skips summary of the workload when services are unavailable', async () => { + const taskStore = taskStoreMock.create({}); + taskStore.aggregate.mockResolvedValue(mockAggregatedResult()); + + const availability$ = new Subject(); + + const workloadAggregator = createWorkloadAggregator( + taskStore, + availability$, + 10, + 3000, + loggingSystemMock.create().get() + ); + + return new Promise(async (resolve) => { + workloadAggregator.pipe(first()).subscribe((result) => { + expect(result.key).toEqual('workload'); + expect(result.value).toMatchObject({ + count: 4, + task_types: { + actions_telemetry: { count: 2, status: { idle: 2 } }, + alerting_telemetry: { count: 1, status: { idle: 1 } }, + session_cleanup: { count: 1, status: { idle: 1 } }, + }, + }); + resolve(); + }); + + availability$.next(false); + + await sleep(10); + expect(taskStore.aggregate).not.toHaveBeenCalled(); + await sleep(10); + expect(taskStore.aggregate).not.toHaveBeenCalled(); + availability$.next(true); + }); + }); + + test('returns a count of the overdue workload', async () => { + const taskStore = taskStoreMock.create({}); + taskStore.aggregate.mockResolvedValue(mockAggregatedResult()); + + const workloadAggregator = createWorkloadAggregator( + taskStore, + of(true), + 10, + 3000, + loggingSystemMock.create().get() + ); + + return new Promise((resolve) => { + workloadAggregator.pipe(first()).subscribe((result) => { + expect(result.key).toEqual('workload'); + expect(result.value).toMatchObject({ + overdue: 6, + }); + resolve(); + }); + }); + }); + + test('returns a histogram of the upcoming workload for the upcoming minute when refresh rate is high', async () => { + const taskStore = taskStoreMock.create({}); + taskStore.aggregate.mockResolvedValue(mockAggregatedResult()); + + const workloadAggregator = createWorkloadAggregator( + taskStore, + of(true), + 10, + 3000, + loggingSystemMock.create().get() + ); + + return new Promise((resolve) => { + workloadAggregator.pipe(first()).subscribe((result) => { + expect(result.key).toEqual('workload'); + expect(result.value).toMatchObject({ + // we have intervals every 3s, so we aggregate buckets 3s apart + // in this mock, Elasticsearch found tasks scheduled in 21 (8th bucket), 24, 27 and 48s seconds from now + // 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57 + // [0, 0, 0, 0, 0, 0, 0, 2, 2, 5, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ] + // Above you see each bucket and the number of scheduled tasks we expect to have in them + estimated_schedule_density: [0, 0, 0, 0, 0, 0, 0, 2, 2, 5, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + }); + resolve(); + }); + }); + }); + + test('returns a histogram of the upcoming workload for twice refresh rate when rate is low', async () => { + const taskStore = taskStoreMock.create({}); + taskStore.aggregate.mockResolvedValue(mockAggregatedResult()); + + const workloadAggregator = createWorkloadAggregator( + taskStore, + of(true), + 60 * 1000, + 3000, + loggingSystemMock.create().get() + ); + + return new Promise((resolve) => { + workloadAggregator.pipe(first()).subscribe(() => { + expect(taskStore.aggregate.mock.calls[0][0]).toMatchObject({ + aggs: { + idleTasks: { + aggs: { + scheduleDensity: { + range: { + field: 'task.runAt', + ranges: [ + { + from: 'now', + to: 'now+2m', + }, + ], + }, + }, + }, + }, + }, + }); + resolve(); + }); + }); + }); + + test('returns a histogram of the upcoming workload maxed out at 50 buckets when rate is too low', async () => { + const taskStore = taskStoreMock.create({}); + taskStore.aggregate.mockResolvedValue(mockAggregatedResult()); + + const workloadAggregator = createWorkloadAggregator( + taskStore, + of(true), + 15 * 60 * 1000, + 3000, + loggingSystemMock.create().get() + ); + + return new Promise((resolve) => { + workloadAggregator.pipe(first()).subscribe((result) => { + expect(taskStore.aggregate.mock.calls[0][0]).toMatchObject({ + aggs: { + idleTasks: { + aggs: { + scheduleDensity: { + range: { + field: 'task.runAt', + ranges: [ + { + from: 'now', + // 50 buckets of 3s = 50 * 3 = 150s + to: 'now+150s', + }, + ], + }, + }, + }, + }, + }, + }); + resolve(); + }); + }); + }); + + test('recovers from errors fetching the workload', async () => { + const taskStore = taskStoreMock.create({}); + taskStore.aggregate + .mockResolvedValueOnce( + setTaskTypeCount(mockAggregatedResult(), 'alerting_telemetry', { + idle: 2, + }) + ) + .mockRejectedValueOnce(new Error('Elasticsearch has gone poof')) + .mockResolvedValueOnce( + setTaskTypeCount(mockAggregatedResult(), 'alerting_telemetry', { + idle: 1, + failed: 1, + }) + ); + const logger = loggingSystemMock.create().get(); + const workloadAggregator = createWorkloadAggregator(taskStore, of(true), 10, 3000, logger); + + return new Promise((resolve, reject) => { + workloadAggregator.pipe(take(2), bufferCount(2)).subscribe((results) => { + expect(results[0].key).toEqual('workload'); + expect(results[0].value).toMatchObject({ + count: 5, + task_types: { + actions_telemetry: { count: 2, status: { idle: 2 } }, + alerting_telemetry: { count: 2, status: { idle: 2 } }, + session_cleanup: { count: 1, status: { idle: 1 } }, + }, + }); + expect(results[1].key).toEqual('workload'); + expect(results[1].value).toMatchObject({ + count: 5, + task_types: { + actions_telemetry: { count: 2, status: { idle: 2 } }, + alerting_telemetry: { count: 2, status: { idle: 1, failed: 1 } }, + session_cleanup: { count: 1, status: { idle: 1 } }, + }, + }); + resolve(); + }, reject); + }); + }); +}); + +describe('estimateRecurringTaskScheduling', () => { + test('flattens out buckets with non recurring tasks', () => { + const now = Date.now(); + const schedule = times(10, (index) => ({ + key: index * 3000 + now, + nonRecurring: index, + })); + expect(estimateRecurringTaskScheduling(schedule, 3000)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + + test('estimates the buckets that recurring tasks might repeat in when recurring task interval equals the interval', () => { + const now = Date.now(); + const schedule: Array<{ + key: number; + nonRecurring: number; + recurring?: Array<[number, string]>; + }> = times(10, (index) => ({ + key: index * 3000 + now, + nonRecurring: 0, + })); + + schedule[0].nonRecurring = 1; + schedule[1].nonRecurring = 1; + schedule[4].recurring = [[1, '3s']]; + + expect(estimateRecurringTaskScheduling(schedule, 3000)).toEqual([1, 1, 0, 0, 1, 1, 1, 1, 1, 1]); + }); + + test('estimates the buckets that recurring tasks might repeat in when recurring task interval is larger than the interval', () => { + const now = Date.now(); + const schedule: Array<{ + key: number; + nonRecurring: number; + recurring?: Array<[number, string]>; + }> = times(10, (index) => ({ + key: index * 3000 + now, + nonRecurring: 0, + })); + + schedule[0].nonRecurring = 1; + schedule[1].nonRecurring = 1; + schedule[4].recurring = [[1, '6s']]; + + expect(estimateRecurringTaskScheduling(schedule, 3000)).toEqual([1, 1, 0, 0, 1, 0, 1, 0, 1, 0]); + }); + + test('estimates the buckets that recurring tasks might repeat in when recurring task interval doesnt divide by interval', () => { + const now = Date.now(); + const schedule: Array<{ + key: number; + nonRecurring: number; + recurring?: Array<[number, string]>; + }> = times(10, (index) => ({ + key: index * 3000 + now, + nonRecurring: 0, + })); + + schedule[0].nonRecurring = 1; + schedule[1].nonRecurring = 1; + schedule[4].recurring = [[1, '5s']]; + + expect(estimateRecurringTaskScheduling(schedule, 3000)).toEqual([1, 1, 0, 0, 1, 0, 1, 0, 1, 0]); + }); + + test('estimates the buckets that recurring tasks might repeat in when recurring tasks overlap', () => { + const now = Date.now(); + const schedule: Array<{ + key: number; + nonRecurring: number; + recurring?: Array<[number, string]>; + }> = times(20, (index) => ({ + key: index * 3000 + now, + nonRecurring: 0, + })); + + schedule[0].nonRecurring = 1; + schedule[1].nonRecurring = 1; + schedule[3].recurring = [[1, '3s']]; + schedule[4].recurring = [ + [2, '6s'], + [1, '8s'], + ]; + schedule[5].recurring = [[1, '5s']]; + schedule[6].nonRecurring = 3; + + expect(estimateRecurringTaskScheduling(schedule, 3000)).toEqual([ + 1, + 1, + 0, + 1, + 4, + 2, + 6, + 3, + 3, + 2, + 4, + 2, + 3, + 3, + 3, + 2, + 4, + 2, + 3, + 3, + ]); + }); +}); + +describe('padBuckets', () => { + test('returns zeroed out bucklets when there are no buckets in the histogram', async () => { + expect( + padBuckets(10, 3000, { + key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', + from: 1601668048128, + from_as_string: '2020-10-02T19:47:28.128Z', + to: 1601668108128, + to_as_string: '2020-10-02T19:48:28.128Z', + doc_count: 0, + histogram: { + buckets: [], + }, + }) + ).toEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + }); + + test('pads buckets with zeros to fill out the entire period of time after detected buckets', async () => { + expect( + padBuckets(10, 3000, { + key: '2020-10-02T19:47:28.128Z-2020-10-02T19:48:28.128Z', + from: 1601668046000, + from_as_string: '2020-10-02T19:47:26.000Z', + to: 1601668076000, + to_as_string: '2020-10-02T19:47:56.000Z', + doc_count: 3, + histogram: { + buckets: [ + { + key_as_string: '2020-10-02T19:47:27.000Z', + key: 1601668047000, + doc_count: 1, + interval: { + sum_other_doc_count: 0, + buckets: [], + }, + }, + { + key_as_string: '2020-10-02T19:47:30.000Z', + key: 1601668050000, + doc_count: 1, + interval: { + sum_other_doc_count: 0, + buckets: [], + }, + }, + { + key_as_string: '2020-10-02T19:47:33.000Z', + key: 1601668053000, + doc_count: 0, + interval: { + sum_other_doc_count: 0, + buckets: [], + }, + }, + { + key_as_string: '2020-10-02T19:47:36.000Z', + key: 1601668056000, + doc_count: 0, + interval: { + sum_other_doc_count: 0, + buckets: [], + }, + }, + { + key_as_string: '2020-10-02T19:47:39.000Z', + key: 1601668059000, + doc_count: 0, + interval: { + sum_other_doc_count: 0, + buckets: [], + }, + }, + { + key_as_string: '2020-10-02T19:47:42.000Z', + key: 1601668062000, + doc_count: 1, + interval: { + sum_other_doc_count: 0, + buckets: [], + }, + }, + ], + }, + }) + ).toEqual([1, 1, 0, 0, 0, 1, 0, 0, 0, 0]); + }); + + test('pads buckets with zeros to fill out the entire period of time before detected buckets', async () => { + expect( + padBuckets(10, 3000, { + key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', + from: 1601671183000, + from_as_string: '2020-10-02T20:39:43.000Z', + to: 1601671213000, + to_as_string: '2020-10-02T20:40:13.000Z', + doc_count: 2, + histogram: { + buckets: [ + { + key_as_string: '2020-10-02T20:40:09.000Z', + key: 1601671209000, + doc_count: 1, + interval: { buckets: [] }, + }, + { + key_as_string: '2020-10-02T20:40:12.000Z', + key: 1601671212000, + doc_count: 1, + interval: { buckets: [] }, + }, + ], + }, + }) + ).toEqual([0, 0, 0, 0, 0, 0, 0, 0, 1, 1]); + }); + + test('pads buckets with zeros to fill out the entire period surounding the detected buckets', async () => { + expect( + padBuckets(20, 3000, { + key: '2020-10-02T20:39:45.793Z-2020-10-02T20:40:14.793Z', + from: 1601671185793, + from_as_string: '2020-10-02T20:39:45.793Z', + to: 1601671245793, + to_as_string: '2020-10-02T20:40:45.793Z', + doc_count: 2, + histogram: { + buckets: [ + { + key_as_string: '2020-10-02T20:40:09.000Z', + key: 1601671209000, + doc_count: 1, + interval: { buckets: [] }, + }, + { + key_as_string: '2020-10-02T20:40:12.000Z', + key: 1601671212000, + doc_count: 1, + interval: { buckets: [] }, + }, + ], + }, + }) + ).toEqual([0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + }); +}); + +function setTaskTypeCount( + { aggregations }: MockESResult, + taskType: string, + status: Record +) { + const taskTypes = aggregations!.taskType as AggregationResultOf< + WorkloadAggregation['aggs']['taskType'], + {} + >; + const buckets = [ + ...taskTypes.buckets.filter(({ key }) => key !== taskType), + { + key: taskType, + doc_count: Object.values(status).reduce((sum, count) => sum + count, 0), + status: { + sum_other_doc_count: 0, + buckets: Object.entries(status).map(([key, count]) => ({ + key, + doc_count: count, + })), + }, + }, + ]; + return ({ + hits: { + total: { value: buckets.reduce((sum, bucket) => sum + bucket.doc_count, 0) }, + }, + aggregations: { + ...aggregations, + taskType: { + sum_other_doc_count: 0, + buckets, + }, + }, + } as {}) as MockESResult; +} + +/** * + * This creates a mock histogram as returned by Elasticsearch + * + * @param from lower bound of query + * @param findFrom the timestamp (key) of the first bucket returned + * @param to upper bound of query + * @param interval the duration that each bucket coresponds to + * @param foundBuckets the buckets identified by ES, any buckets missing before or after which + * are still in the date range are assumed to have 0 results, ES only returns 0 for + * buckets that sit in between buckets which do have results + */ +function mockHistogram( + from: number, + findFrom: number, + to: number, + interval: number, + foundBuckets: Array +) { + const now = Date.now(); + const fromDate = new Date(now + from); + const toDate = new Date(now + to); + return { + key: `${fromDate.toISOString()}-${toDate.toISOString()}`, + from: now + from, + from_as_string: fromDate.toISOString(), + to: now + to, + to_as_string: toDate.toISOString(), + doc_count: foundBuckets.reduce((sum: number, count) => sum + (count ?? 0), 0), + histogram: { + buckets: foundBuckets.reduce( + (histogramBuckets, count, index) => { + if (typeof count === 'number') { + const key = new Date(now + findFrom + index * interval); + histogramBuckets.push({ + key_as_string: key.toISOString(), + key: key.getTime(), + doc_count: count, + interval: { buckets: [] }, + }); + } + return histogramBuckets; + }, + [] as Array<{ + key_as_string: string; + key: number; + doc_count: number; + interval: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; + }> + ), + }, + }; +} diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts new file mode 100644 index 0000000000000..17448ea412ae6 --- /dev/null +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -0,0 +1,346 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineLatest, Observable, timer } from 'rxjs'; +import { mergeMap, map, filter, catchError } from 'rxjs/operators'; +import { Logger } from 'src/core/server'; +import { JsonObject } from 'src/plugins/kibana_utils/common'; +import { keyBy, mapValues } from 'lodash'; +import { AggregatedStatProvider } from './runtime_statistics_aggregator'; +import { parseIntervalAsSecond, asInterval, parseIntervalAsMillisecond } from '../lib/intervals'; +import { AggregationResultOf } from '../../../apm/typings/elasticsearch/aggregations'; +import { HealthStatus } from './monitoring_stats_stream'; +import { TaskStore } from '../task_store'; + +interface StatusStat extends JsonObject { + [status: string]: number; +} +interface TaskTypeStat extends JsonObject { + [taskType: string]: { + count: number; + status: StatusStat; + }; +} + +export interface WorkloadStat extends JsonObject { + count: number; + task_types: TaskTypeStat; + schedule: Array<[string, number]>; + overdue: number; + estimated_schedule_density: number[]; +} + +export interface WorkloadAggregation { + aggs: { + taskType: { + terms: { field: string }; + aggs: { + status: { + terms: { field: string }; + }; + }; + }; + schedule: { + terms: { field: string }; + }; + idleTasks: { + filter: { + term: { 'task.status': string }; + }; + aggs: { + scheduleDensity: { + range: { + field: string; + ranges: [{ from: string; to: string }]; + }; + aggs: { + histogram: { + date_histogram: { + field: string; + fixed_interval: string; + }; + aggs: { + interval: { + terms: { field: string }; + }; + }; + }; + }; + }; + overdue: { + filter: { + range: { + 'task.runAt': { lt: string }; + }; + }; + }; + }; + }; + }; +} + +// The type of a bucket in the scheduleDensity range aggregation +type ScheduleDensityResult = AggregationResultOf< + WorkloadAggregation['aggs']['idleTasks']['aggs']['scheduleDensity'], + {} +>['buckets'][0]; +type ScheduledIntervals = ScheduleDensityResult['histogram']['buckets'][0]; + +// Set an upper bound just in case a customer sets a really high refresh rate +const MAX_SHCEDULE_DENSITY_BUCKETS = 50; + +export function createWorkloadAggregator( + taskStore: TaskStore, + elasticsearchAndSOAvailability$: Observable, + refreshInterval: number, + pollInterval: number, + logger: Logger +): AggregatedStatProvider { + // calculate scheduleDensity going two refreshIntervals or 1 minute into into the future + // (the longer of the two) + const scheduleDensityBuckets = Math.min( + Math.max(Math.round(60000 / pollInterval), Math.round((refreshInterval * 2) / pollInterval)), + MAX_SHCEDULE_DENSITY_BUCKETS + ); + + return combineLatest([timer(0, refreshInterval), elasticsearchAndSOAvailability$]).pipe( + filter(([, areElasticsearchAndSOAvailable]) => areElasticsearchAndSOAvailable), + mergeMap(() => + taskStore.aggregate({ + aggs: { + taskType: { + terms: { field: 'task.taskType' }, + aggs: { + status: { + terms: { field: 'task.status' }, + }, + }, + }, + schedule: { + terms: { field: 'task.schedule.interval' }, + }, + idleTasks: { + filter: { + term: { 'task.status': 'idle' }, + }, + aggs: { + scheduleDensity: { + // create a window of upcoming tasks + range: { + field: 'task.runAt', + ranges: [ + { + from: `now`, + to: `now+${asInterval(scheduleDensityBuckets * pollInterval)}`, + }, + ], + }, + aggs: { + // create histogram of scheduling in the window, with each bucket being a polling interval + histogram: { + date_histogram: { + field: 'task.runAt', + fixed_interval: asInterval(pollInterval), + }, + // break down each bucket in the historgram by schedule + aggs: { + interval: { + terms: { field: 'task.schedule.interval' }, + }, + }, + }, + }, + }, + overdue: { + filter: { + range: { + 'task.runAt': { lt: 'now' }, + }, + }, + }, + }, + }, + }, + }) + ), + map((result) => { + const { + aggregations, + hits: { + total: { value: count }, + }, + } = result; + + if ( + !( + aggregations?.taskType && + aggregations?.schedule && + aggregations?.idleTasks?.overdue && + aggregations?.idleTasks?.scheduleDensity + ) + ) { + throw new Error(`Invalid workload: ${JSON.stringify(result)}`); + } + + const taskTypes = aggregations.taskType.buckets; + const schedules = aggregations.schedule.buckets; + + const { + overdue: { doc_count: overdue }, + scheduleDensity: { buckets: [scheduleDensity] = [] } = {}, + } = aggregations.idleTasks; + + const summary: WorkloadStat = { + count, + task_types: mapValues(keyBy(taskTypes, 'key'), ({ doc_count: docCount, status }) => { + return { + count: docCount, + status: mapValues(keyBy(status.buckets, 'key'), 'doc_count'), + }; + }), + schedule: schedules + .sort( + (scheduleLeft, scheduleRight) => + parseIntervalAsSecond(scheduleLeft.key as string) - + parseIntervalAsSecond(scheduleRight.key as string) + ) + .map((schedule) => [schedule.key as string, schedule.doc_count]), + overdue, + estimated_schedule_density: padBuckets( + scheduleDensityBuckets, + pollInterval, + scheduleDensity + ), + }; + return { + key: 'workload', + value: summary, + }; + }), + catchError((ex: Error, caught) => { + logger.error(`[WorkloadAggregator]: ${ex}`); + // continue to pull values from the same observable + return caught; + }) + ); +} + +interface IntervalTaskCountTouple { + nonRecurring?: number; + recurring?: Array<[number, string]>; + key: number; +} + +export function padBuckets( + scheduleDensityBuckets: number, + pollInterval: number, + scheduleDensity: ScheduleDensityResult +): number[] { + if (scheduleDensity.from && scheduleDensity.to && scheduleDensity.histogram?.buckets?.length) { + const { histogram, from, to } = scheduleDensity; + const firstBucket = histogram.buckets[0].key; + const lastBucket = histogram.buckets[histogram.buckets.length - 1].key; + + const bucketsToPadBeforeFirstBucket = calculateBucketsBetween(firstBucket, from, pollInterval); + const bucketsToPadAfterLast = calculateBucketsBetween( + lastBucket + pollInterval, + to, + pollInterval + ); + + return estimateRecurringTaskScheduling( + [ + ...bucketsToPadBeforeFirstBucket, + ...histogram.buckets.map(countByIntervalInBucket), + ...bucketsToPadAfterLast, + ], + pollInterval + ); + } + return new Array(scheduleDensityBuckets).fill(0); +} + +function countByIntervalInBucket(bucket: ScheduledIntervals): IntervalTaskCountTouple { + if (bucket.doc_count === 0) { + return { nonRecurring: 0, key: bucket.key }; + } + const recurring: Array<[number, string]> = []; + let nonRecurring = bucket.doc_count; + for (const intervalBucket of bucket.interval.buckets) { + recurring.push([intervalBucket.doc_count, intervalBucket.key as string]); + nonRecurring -= intervalBucket.doc_count; + } + + return { nonRecurring, recurring, key: bucket.key }; +} + +function calculateBucketsBetween( + from: number, + to: number, + interval: number, + bucketInterval: number = interval +): Array<{ key: number }> { + const calcForwardInTime = from < to; + + // as task interval might not divide by the pollInterval (aka the bucket interval) + // we have to adjust for the "drift" that occurs when estimating when the next + // bucket the task might actually get scheduled in + const actualInterval = Math.ceil(interval / bucketInterval) * bucketInterval; + + const buckets: Array<{ key: number }> = []; + const toBound = calcForwardInTime ? to : -(to + actualInterval); + let fromBound = calcForwardInTime ? from : -from; + + while (fromBound < toBound) { + buckets.push({ key: fromBound }); + fromBound += actualInterval; + } + + return calcForwardInTime + ? buckets + : buckets.reverse().map((bucket) => { + bucket.key = Math.abs(bucket.key); + return bucket; + }); +} + +export function estimateRecurringTaskScheduling( + scheduleDensity: IntervalTaskCountTouple[], + pollInterval: number +) { + const lastKey = scheduleDensity[scheduleDensity.length - 1].key; + + return scheduleDensity.map((bucket, currentBucketIndex) => { + for (const [count, interval] of bucket.recurring ?? []) { + for (const recurrance of calculateBucketsBetween( + bucket.key, + // `calculateBucketsBetween` uses the `to` as a non-inclusive upper bound + // but lastKey is a bucket we wish to include + lastKey + pollInterval, + parseIntervalAsMillisecond(interval), + pollInterval + )) { + const recurranceBucketIndex = + currentBucketIndex + Math.ceil((recurrance.key - bucket.key) / pollInterval); + + if (recurranceBucketIndex < scheduleDensity.length) { + scheduleDensity[recurranceBucketIndex].nonRecurring = + count + (scheduleDensity[recurranceBucketIndex].nonRecurring ?? 0); + } + } + } + return bucket.nonRecurring ?? 0; + }); +} + +export function summarizeWorkloadStat( + workloadStats: WorkloadStat +): { value: WorkloadStat; status: HealthStatus } { + return { + value: workloadStats, + status: HealthStatus.OK, + }; +} diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 50e7e9a7aa197..9a1d83f6195ab 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TaskManagerPlugin } from './plugin'; +import { TaskManagerPlugin, getElasticsearchAndSOAvailability } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { TaskManagerConfig } from './config'; +import { Subject } from 'rxjs'; +import { bufferCount, take } from 'rxjs/operators'; +import { CoreStatus, ServiceStatusLevels } from 'src/core/server'; describe('TaskManagerPlugin', () => { describe('setup', () => { @@ -19,6 +22,16 @@ describe('TaskManagerPlugin', () => { poll_interval: 3000, max_poll_inactivity_cycles: 10, request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_required_freshness: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, }); pluginInitializerContext.env.instanceUuid = ''; @@ -38,6 +51,16 @@ describe('TaskManagerPlugin', () => { poll_interval: 3000, max_poll_inactivity_cycles: 10, request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_required_freshness: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, }); const taskManagerPlugin = new TaskManagerPlugin(pluginInitializerContext); @@ -68,4 +91,99 @@ describe('TaskManagerPlugin', () => { ); }); }); + + describe('getElasticsearchAndSOAvailability', () => { + test('returns true when both services are available', async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(1), bufferCount(1)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: true })); + + expect(await availability).toEqual([true]); + }); + + test('returns false when both services are unavailable', async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(1), bufferCount(1)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: false, savedObjects: false })); + + expect(await availability).toEqual([false]); + }); + + test('returns false when one service is unavailable but the other is available', async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(1), bufferCount(1)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: false })); + + expect(await availability).toEqual([false]); + }); + + test('shift back and forth between values as status changes', async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(3), bufferCount(3)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: false })); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: true })); + + core$.next(mockCoreStatusAvailability({ elasticsearch: false, savedObjects: false })); + + expect(await availability).toEqual([false, true, false]); + }); + + test(`skips values when the status hasn't changed`, async () => { + const core$ = new Subject(); + + const availability = getElasticsearchAndSOAvailability(core$) + .pipe(take(3), bufferCount(3)) + .toPromise(); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: false })); + + // still false, so shouldn't emit a second time + core$.next(mockCoreStatusAvailability({ elasticsearch: false, savedObjects: true })); + + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: true })); + + // shouldn't emit as already true + core$.next(mockCoreStatusAvailability({ elasticsearch: true, savedObjects: true })); + + core$.next(mockCoreStatusAvailability({ elasticsearch: false, savedObjects: false })); + + expect(await availability).toEqual([false, true, false]); + }); + }); }); + +function mockCoreStatusAvailability({ + elasticsearch, + savedObjects, +}: { + elasticsearch: boolean; + savedObjects: boolean; +}) { + return { + elasticsearch: { + level: elasticsearch ? ServiceStatusLevels.available : ServiceStatusLevels.unavailable, + summary: '', + }, + savedObjects: { + level: savedObjects ? ServiceStatusLevels.available : ServiceStatusLevels.unavailable, + summary: '', + }, + }; +} diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 0381698e6fb77..70688cd169d7e 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -3,8 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext, Plugin, CoreSetup, Logger, CoreStart } from 'src/core/server'; -import { first } from 'rxjs/operators'; +import { combineLatest, Observable, Subject } from 'rxjs'; +import { first, map, distinctUntilChanged } from 'rxjs/operators'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + Logger, + CoreStart, + ServiceStatusLevels, + CoreStatus, +} from '../../../../src/core/server'; import { TaskDefinition } from './task'; import { TaskPollingLifecycle } from './polling_lifecycle'; import { TaskManagerConfig } from './config'; @@ -14,6 +23,8 @@ import { TaskTypeDictionary } from './task_type_dictionary'; import { FetchResult, SearchOpts, TaskStore } from './task_store'; import { createManagedConfiguration } from './lib/create_managed_configuration'; import { TaskScheduling } from './task_scheduling'; +import { healthRoute } from './routes'; +import { createMonitoringStats, MonitoringStats } from './monitoring'; export type TaskManagerSetupContract = { addMiddleware: (middleware: Middleware) => void } & Pick< TaskTypeDictionary, @@ -34,6 +45,8 @@ export class TaskManagerPlugin private logger: Logger; private definitions: TaskTypeDictionary; private middleware: Middleware = createInitialMiddleware(); + private elasticsearchAndSOAvailability$?: Observable; + private monitoringStats$ = new Subject(); constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; @@ -41,13 +54,15 @@ export class TaskManagerPlugin this.definitions = new TaskTypeDictionary(this.logger); } - public async setup({ savedObjects }: CoreSetup): Promise { + public async setup(core: CoreSetup): Promise { this.config = await this.initContext.config .create() .pipe(first()) .toPromise(); - setupSavedObjects(savedObjects, this.config); + this.elasticsearchAndSOAvailability$ = getElasticsearchAndSOAvailability(core.status.core$); + + setupSavedObjects(core.savedObjects, this.config); this.taskManagerId = this.initContext.env.instanceUuid; if (!this.taskManagerId) { @@ -59,6 +74,26 @@ export class TaskManagerPlugin this.logger.info(`TaskManager is identified by the Kibana UUID: ${this.taskManagerId}`); } + // Routes + const router = core.http.createRouter(); + const serviceStatus$ = healthRoute( + router, + this.monitoringStats$, + this.logger, + this.taskManagerId, + this.config! + ); + + core.getStartServices().then(async () => { + core.status.set( + combineLatest([core.status.derivedStatus$, serviceStatus$]).pipe( + map(([derivedStatus, serviceStatus]) => + serviceStatus.level > derivedStatus.level ? serviceStatus : derivedStatus + ) + ) + ); + }); + return { addMiddleware: (middleware: Middleware) => { this.assertStillInSetup('add Middleware'); @@ -84,34 +119,39 @@ export class TaskManagerPlugin taskManagerId: `kibana:${this.taskManagerId!}`, }); - const { maxWorkersConfiguration$, pollIntervalConfiguration$ } = createManagedConfiguration({ + const managedConfiguration = createManagedConfiguration({ logger: this.logger, errors$: taskStore.errors$, startingMaxWorkers: this.config!.max_workers, startingPollInterval: this.config!.poll_interval, }); - const taskPollingLifecycle = new TaskPollingLifecycle({ + this.taskPollingLifecycle = new TaskPollingLifecycle({ config: this.config!, definitions: this.definitions, logger: this.logger, taskStore, middleware: this.middleware, - maxWorkersConfiguration$, - pollIntervalConfiguration$, + elasticsearchAndSOAvailability$: this.elasticsearchAndSOAvailability$!, + ...managedConfiguration, }); - this.taskPollingLifecycle = taskPollingLifecycle; + + createMonitoringStats( + this.taskPollingLifecycle, + taskStore, + this.elasticsearchAndSOAvailability$!, + this.config!, + managedConfiguration, + this.logger + ).subscribe((stat) => this.monitoringStats$.next(stat)); const taskScheduling = new TaskScheduling({ logger: this.logger, taskStore, middleware: this.middleware, - taskPollingLifecycle, + taskPollingLifecycle: this.taskPollingLifecycle, }); - // start polling for work - taskPollingLifecycle.start(); - return { fetch: (opts: SearchOpts): Promise => taskStore.fetch(opts), get: (id: string) => taskStore.get(id), @@ -122,12 +162,6 @@ export class TaskManagerPlugin }; } - public stop() { - if (this.taskPollingLifecycle) { - this.taskPollingLifecycle.stop(); - } - } - /** * Ensures task manager hasn't started * @@ -140,3 +174,16 @@ export class TaskManagerPlugin } } } + +export function getElasticsearchAndSOAvailability( + core$: Observable +): Observable { + return core$.pipe( + map( + ({ elasticsearch, savedObjects }) => + elasticsearch.level === ServiceStatusLevels.available && + savedObjects.level === ServiceStatusLevels.available + ), + distinctUntilChanged() + ); +} diff --git a/x-pack/plugins/task_manager/server/polling/task_poller.test.ts b/x-pack/plugins/task_manager/server/polling/task_poller.test.ts index 956c8b05f3860..f5f1667312d79 100644 --- a/x-pack/plugins/task_manager/server/polling/task_poller.test.ts +++ b/x-pack/plugins/task_manager/server/polling/task_poller.test.ts @@ -9,7 +9,8 @@ import { Subject, of, BehaviorSubject } from 'rxjs'; import { Option, none, some } from 'fp-ts/lib/Option'; import { createTaskPoller, PollingError, PollingErrorType } from './task_poller'; import { fakeSchedulers } from 'rxjs-marbles/jest'; -import { sleep, resolvable, Resolvable, mockLogger } from '../test_utils'; +import { sleep, resolvable, Resolvable } from '../test_utils'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { asOk, asErr } from '../lib/result_type'; describe('TaskPoller', () => { @@ -24,7 +25,7 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, getCapacity: () => 1, @@ -59,7 +60,7 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$, bufferCapacity, getCapacity: () => 1, @@ -101,7 +102,7 @@ describe('TaskPoller', () => { let hasCapacity = true; createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, work, @@ -160,7 +161,7 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); const pollRequests$ = new Subject>(); createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, work, @@ -206,7 +207,7 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); const pollRequests$ = new Subject>(); createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, work, @@ -251,7 +252,7 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); const pollRequests$ = new Subject>(); createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, work, @@ -288,7 +289,7 @@ describe('TaskPoller', () => { const handler = jest.fn(); const pollRequests$ = new Subject>(); createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, work: async (...args) => { @@ -339,7 +340,7 @@ describe('TaskPoller', () => { type ResolvableTupple = [string, PromiseLike & Resolvable]; const pollRequests$ = new Subject>(); createTaskPoller<[string, Resolvable], string[]>({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, work: async (...resolvables) => { @@ -399,7 +400,7 @@ describe('TaskPoller', () => { const handler = jest.fn(); const pollRequests$ = new Subject>(); createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, work: async (...args) => { @@ -440,7 +441,7 @@ describe('TaskPoller', () => { return callCount; }); createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, work, @@ -483,7 +484,7 @@ describe('TaskPoller', () => { const work = jest.fn(async () => {}); const pollRequests$ = new Subject>(); createTaskPoller({ - logger: mockLogger(), + logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), bufferCapacity, work, diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts index 9df1e06165bc6..286e29194d6e6 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.mock.ts @@ -10,7 +10,6 @@ import { of, Observable } from 'rxjs'; export const taskPollingLifecycleMock = { create(opts: { isStarted?: boolean; events$?: Observable }) { return ({ - start: jest.fn(), attemptToRun: jest.fn(), get isStarted() { return opts.isStarted ?? true; @@ -18,7 +17,6 @@ export const taskPollingLifecycleMock = { get events() { return opts.events$ ?? of(); }, - stop: jest.fn(), } as unknown) as jest.Mocked; }, }; diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index 29c8e836303f8..0f807976970cf 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import sinon from 'sinon'; -import { of } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { TaskPollingLifecycle, claimAvailableTasks } from './polling_lifecycle'; import { createInitialMiddleware } from './lib/middleware'; @@ -28,6 +28,16 @@ describe('TaskPollingLifecycle', () => { poll_interval: 6000000, max_poll_inactivity_cycles: 10, request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_required_freshness: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, }, taskStore: mockTaskStore, logger: taskManagerLogger, @@ -45,15 +55,64 @@ describe('TaskPollingLifecycle', () => { afterEach(() => clock.restore()); describe('start', () => { - test('begins polling once start is called', () => { - const taskManager = new TaskPollingLifecycle(taskManagerOpts); + test('begins polling once the ES and SavedObjects services are available', () => { + const elasticsearchAndSOAvailability$ = new Subject(); + new TaskPollingLifecycle({ + elasticsearchAndSOAvailability$, + ...taskManagerOpts, + }); + + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).not.toHaveBeenCalled(); + + elasticsearchAndSOAvailability$.next(true); + + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).toHaveBeenCalled(); + }); + }); + + describe('stop', () => { + test('stops polling once the ES and SavedObjects services become unavailable', () => { + const elasticsearchAndSOAvailability$ = new Subject(); + new TaskPollingLifecycle({ + elasticsearchAndSOAvailability$, + ...taskManagerOpts, + }); + + elasticsearchAndSOAvailability$.next(true); + + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).toHaveBeenCalled(); + elasticsearchAndSOAvailability$.next(false); + + mockTaskStore.claimAvailableTasks.mockClear(); clock.tick(150); expect(mockTaskStore.claimAvailableTasks).not.toHaveBeenCalled(); + }); + + test('restarts polling once the ES and SavedObjects services become available again', () => { + const elasticsearchAndSOAvailability$ = new Subject(); + new TaskPollingLifecycle({ + elasticsearchAndSOAvailability$, + ...taskManagerOpts, + }); + + elasticsearchAndSOAvailability$.next(true); - taskManager.start(); + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).toHaveBeenCalled(); + elasticsearchAndSOAvailability$.next(false); + mockTaskStore.claimAvailableTasks.mockClear(); clock.tick(150); + + expect(mockTaskStore.claimAvailableTasks).not.toHaveBeenCalled(); + + elasticsearchAndSOAvailability$.next(true); + clock.tick(150); + expect(mockTaskStore.claimAvailableTasks).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.ts index 8a506cca699de..ccba750401f28 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.ts @@ -9,6 +9,7 @@ import { performance } from 'perf_hooks'; import { pipe } from 'fp-ts/lib/pipeable'; import { Option, some, map as mapOptional } from 'fp-ts/lib/Option'; +import { tap } from 'rxjs/operators'; import { Logger } from '../../../../src/core/server'; import { Result, asErr, mapErr } from './lib/result_type'; @@ -21,6 +22,8 @@ import { TaskClaim, TaskRunRequest, asTaskRunRequestEvent, + TaskPollingCycle, + asTaskPollingCycleEvent, } from './task_events'; import { fillPool, FillPoolResult } from './lib/fill_pool'; import { Middleware } from './lib/middleware'; @@ -45,9 +48,15 @@ export type TaskPollingLifecycleOpts = { taskStore: TaskStore; config: TaskManagerConfig; middleware: Middleware; + elasticsearchAndSOAvailability$: Observable; } & ManagedConfiguration; -export type TaskLifecycleEvent = TaskMarkRunning | TaskRun | TaskClaim | TaskRunRequest; +export type TaskLifecycleEvent = + | TaskMarkRunning + | TaskRun + | TaskClaim + | TaskRunRequest + | TaskPollingCycle; /** * The public interface into the task manager system. @@ -64,8 +73,6 @@ export class TaskPollingLifecycle { private events$ = new Subject(); // all on-demand requests we wish to pipe into the poller private claimRequests$ = new Subject>(); - // the task poller that polls for work on fixed intervals and on demand - private poller$: Observable>>; // our subscription to the poller private pollingSubscription: Subscription = Subscription.EMPTY; @@ -76,36 +83,50 @@ export class TaskPollingLifecycle { * enabling the task manipulation methods, and beginning the background polling * mechanism. */ - constructor(opts: TaskPollingLifecycleOpts) { - const { logger, middleware, maxWorkersConfiguration$, pollIntervalConfiguration$ } = opts; + constructor({ + logger, + middleware, + maxWorkersConfiguration$, + pollIntervalConfiguration$, + // Elasticsearch and SavedObjects availability status + elasticsearchAndSOAvailability$, + config, + taskStore, + definitions, + }: TaskPollingLifecycleOpts) { this.logger = logger; this.middleware = middleware; + this.definitions = definitions; + this.store = taskStore; - this.definitions = opts.definitions; - this.store = opts.taskStore; // pipe store events into the lifecycle event stream this.store.events.subscribe((event) => this.events$.next(event)); this.bufferedStore = new BufferedTaskStore(this.store, { - bufferMaxOperations: opts.config.max_workers, - logger: this.logger, + bufferMaxOperations: config.max_workers, + logger, }); this.pool = new TaskPool({ - logger: this.logger, + logger, maxWorkers$: maxWorkersConfiguration$, }); const { max_poll_inactivity_cycles: maxPollInactivityCycles, poll_interval: pollInterval, - } = opts.config; - this.poller$ = createObservableMonitor>, Error>( + } = config; + + // the task poller that polls for work on fixed intervals and on demand + const poller$: Observable + >> = createObservableMonitor>, Error>( () => createTaskPoller({ - logger: this.logger, + logger, pollInterval$: pollIntervalConfiguration$, - bufferCapacity: opts.config.request_capacity, + bufferCapacity: config.request_capacity, getCapacity: () => this.pool.availableWorkers, pollRequests$: this.claimRequests$, work: this.pollForWork, @@ -125,10 +146,20 @@ export class TaskPollingLifecycle { // operation than just timing out the `work` internally) inactivityTimeout: pollInterval * (maxPollInactivityCycles + 1), onError: (error) => { - this.logger.error(`[Task Poller Monitor]: ${error.message}`); + logger.error(`[Task Poller Monitor]: ${error.message}`); }, } ); + + elasticsearchAndSOAvailability$.subscribe((areESAndSOAvailable) => { + if (areESAndSOAvailable && !this.isStarted) { + // start polling for work + this.pollingSubscription = this.subscribeToPoller(poller$); + } else if (!areESAndSOAvailable && this.isStarted) { + this.pollingSubscription.unsubscribe(); + this.pool.cancelRunningTasks(); + } + }); } public get events(): Observable { @@ -176,33 +207,24 @@ export class TaskPollingLifecycle { ); }; - /** - * Starts up the task manager and starts picking up tasks. - */ - public start() { - if (!this.isStarted) { - this.pollingSubscription = this.poller$.subscribe( - mapErr((error: PollingError) => { - if (error.type === PollingErrorType.RequestCapacityReached) { - pipe( - error.data, - mapOptional((id) => this.emitEvent(asTaskRunRequestEvent(id, asErr(error)))) - ); - } - this.logger.error(error.message); - }) - ); - } - } - - /** - * Stops the task manager and cancels running tasks. - */ - public stop() { - if (this.isStarted) { - this.pollingSubscription.unsubscribe(); - this.pool.cancelRunningTasks(); - } + private subscribeToPoller(poller$: Observable>>) { + return poller$ + .pipe( + tap( + mapErr((error: PollingError) => { + if (error.type === PollingErrorType.RequestCapacityReached) { + pipe( + error.data, + mapOptional((id) => this.emitEvent(asTaskRunRequestEvent(id, asErr(error)))) + ); + } + this.logger.error(error.message); + }) + ) + ) + .subscribe((event: Result>) => { + this.emitEvent(asTaskPollingCycleEvent(event)); + }); } } diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts index 7cdbd8b11bb06..8a94ae4ed82f5 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts @@ -5,21 +5,12 @@ */ import _ from 'lodash'; -import { - asUpdateByQuery, - shouldBeOneOf, - mustBeAllOf, - ExistsFilter, - TermFilter, - RangeFilter, -} from './query_clauses'; +import { asUpdateByQuery, shouldBeOneOf, mustBeAllOf } from './query_clauses'; import { - updateFields, + updateFieldsAndMarkAsFailed, IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt, - TaskWithSchedule, - taskWithLessThanMaxAttempts, SortByRunAtAndRetryAt, } from './mark_available_tasks_as_claimed'; @@ -40,29 +31,29 @@ describe('mark_available_tasks_as_claimed', () => { createTaskRunner: () => ({ run: () => Promise.resolve() }), }, }); + const claimTasksById = undefined; const defaultMaxAttempts = 1; const taskManagerId = '3478fg6-82374f6-83467gf5-384g6f'; const claimOwnershipUntil = '2019-02-12T21:01:22.479Z'; + const fieldUpdates = { + ownerId: taskManagerId, + retryAt: claimOwnershipUntil, + }; expect( asUpdateByQuery({ query: mustBeAllOf( // Either a task with idle status and runAt <= now or // status running or claiming with a retryAt <= now. - shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt), - // Either task has an schedule or the attempts < the maximum configured - shouldBeOneOf( - TaskWithSchedule, - ...Array.from(definitions).map(([type, { maxAttempts }]) => - taskWithLessThanMaxAttempts(type, maxAttempts || defaultMaxAttempts) - ) - ) + shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt) + ), + update: updateFieldsAndMarkAsFailed( + fieldUpdates, + claimTasksById || [], + Array.from(definitions).reduce((accumulator, [type, { maxAttempts }]) => { + return { ...accumulator, [type]: maxAttempts || defaultMaxAttempts }; + }, {}) ), - update: updateFields({ - ownerId: taskManagerId, - status: 'claiming', - retryAt: claimOwnershipUntil, - }), sort: SortByRunAtAndRetryAt, }) ).toEqual({ @@ -100,42 +91,6 @@ describe('mark_available_tasks_as_claimed', () => { ], }, }, - // Either task has an recurring schedule or the attempts < the maximum configured - { - bool: { - should: [ - { exists: { field: 'task.schedule' } }, - { - bool: { - must: [ - { term: { 'task.taskType': 'sampleTask' } }, - { - range: { - 'task.attempts': { - lt: 5, - }, - }, - }, - ], - }, - }, - { - bool: { - must: [ - { term: { 'task.taskType': 'otherTask' } }, - { - range: { - 'task.attempts': { - lt: 1, - }, - }, - }, - ], - }, - }, - ], - }, - }, ], }, }, @@ -158,12 +113,26 @@ if (doc['task.runAt'].size()!=0) { }, seq_no_primary_term: true, script: { - source: `ctx._source.task.ownerId=params.ownerId; ctx._source.task.status=params.status; ctx._source.task.retryAt=params.retryAt;`, + source: ` + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } + `, lang: 'painless', params: { - ownerId: taskManagerId, - retryAt: claimOwnershipUntil, - status: 'claiming', + fieldUpdates: { + ownerId: taskManagerId, + retryAt: claimOwnershipUntil, + }, + claimTasksById: [], + taskMaxAttempts: { + sampleTask: 5, + otherTask: 1, + }, }, }, }); diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index 699af3ed07d5d..072ec4648201a 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -100,12 +100,26 @@ if (doc['task.runAt'].size()!=0) { }, }; -export const updateFields = (fieldUpdates: { - [field: string]: string | number | Date; -}): ScriptClause => ({ - source: Object.keys(fieldUpdates) - .map((field) => `ctx._source.task.${field}=params.${field};`) - .join(' '), +export const updateFieldsAndMarkAsFailed = ( + fieldUpdates: { + [field: string]: string | number | Date; + }, + claimTasksById: string[], + taskMaxAttempts: { [field: string]: number } +): ScriptClause => ({ + source: ` + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } + `, lang: 'painless', - params: fieldUpdates, + params: { + fieldUpdates, + claimTasksById, + taskMaxAttempts, + }, }); diff --git a/x-pack/plugins/task_manager/server/queries/query_clauses.ts b/x-pack/plugins/task_manager/server/queries/query_clauses.ts index f16ee302d6c83..5503b9cd94105 100644 --- a/x-pack/plugins/task_manager/server/queries/query_clauses.ts +++ b/x-pack/plugins/task_manager/server/queries/query_clauses.ts @@ -165,7 +165,12 @@ export interface ScriptClause { source: string; lang: string; params: { - [field: string]: string | number | Date; + [field: string]: + | string + | number + | Date + | string[] + | { [field: string]: string | number | Date }; }; } diff --git a/x-pack/plugins/task_manager/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/task_manager/server/routes/_mock_handler_arguments.ts new file mode 100644 index 0000000000000..2affbc7dbf7ee --- /dev/null +++ b/x-pack/plugins/task_manager/server/routes/_mock_handler_arguments.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { MethodKeysOf } from '@kbn/utility-types'; +import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; +import { identity } from 'lodash'; +import { httpServerMock } from '../../../../../src/core/server/mocks'; + +export function mockHandlerArguments( + {}: {}, + req: unknown, + res?: Array> +): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { + return [ + ({} as unknown) as RequestHandlerContext, + req as KibanaRequest, + mockResponseFactory(res), + ]; +} + +export const mockResponseFactory = (resToMock: Array> = []) => { + const factory: jest.Mocked = httpServerMock.createResponseFactory(); + resToMock.forEach((key: string) => { + if (key in factory) { + Object.defineProperty(factory, key, { + value: jest.fn(identity), + }); + } + }); + return (factory as unknown) as KibanaResponseFactory; +}; diff --git a/x-pack/plugins/task_manager/server/routes/health.test.ts b/x-pack/plugins/task_manager/server/routes/health.test.ts new file mode 100644 index 0000000000000..5a0cef8eda94b --- /dev/null +++ b/x-pack/plugins/task_manager/server/routes/health.test.ts @@ -0,0 +1,393 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable, of, Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { merge } from 'lodash'; +import uuid from 'uuid'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { healthRoute } from './health'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { sleep } from '../test_utils'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { Logger } from '../../../../../src/core/server'; +import { MonitoringStats, summarizeMonitoringStats } from '../monitoring'; +import { ServiceStatusLevels } from 'src/core/server'; +import { configSchema, TaskManagerConfig } from '../config'; + +describe('healthRoute', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('registers the route', async () => { + const router = httpServiceMock.createRouter(); + + const logger = loggingSystemMock.create().get(); + healthRoute(router, of(), logger, uuid.v4(), getTaskManagerConfig()); + + const [config] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/task_manager/_health"`); + }); + + it('logs the Task Manager stats at a fixed interval', async () => { + const router = httpServiceMock.createRouter(); + const logger = loggingSystemMock.create().get(); + + const mockStat = mockHealthStats(); + await sleep(10); + const skippedMockStat = mockHealthStats(); + await sleep(10); + const nextMockStat = mockHealthStats(); + + const stats$ = new Subject(); + + const id = uuid.v4(); + healthRoute( + router, + stats$, + logger, + id, + getTaskManagerConfig({ + monitored_stats_required_freshness: 1000, + monitored_aggregated_stats_refresh_rate: 60000, + }) + ); + + stats$.next(mockStat); + await sleep(500); + stats$.next(skippedMockStat); + await sleep(600); + stats$.next(nextMockStat); + + const firstDebug = JSON.parse( + (logger as jest.Mocked).debug.mock.calls[0][0].replace('Latest Monitored Stats: ', '') + ); + expect(firstDebug).toMatchObject({ + id, + timestamp: expect.any(String), + status: expect.any(String), + ...summarizeMonitoringStats(mockStat, getTaskManagerConfig({})), + }); + + const secondDebug = JSON.parse( + (logger as jest.Mocked).debug.mock.calls[1][0].replace('Latest Monitored Stats: ', '') + ); + expect(secondDebug).not.toMatchObject({ + id, + timestamp: expect.any(String), + status: expect.any(String), + ...summarizeMonitoringStats(skippedMockStat, getTaskManagerConfig({})), + }); + expect(secondDebug).toMatchObject({ + id, + timestamp: expect.any(String), + status: expect.any(String), + ...summarizeMonitoringStats(nextMockStat, getTaskManagerConfig({})), + }); + + expect(logger.debug).toHaveBeenCalledTimes(2); + }); + + it('returns a error status if the overall stats have not been updated within the required hot freshness', async () => { + const router = httpServiceMock.createRouter(); + + const stats$ = new Subject(); + + const serviceStatus$ = healthRoute( + router, + stats$, + loggingSystemMock.create().get(), + uuid.v4(), + getTaskManagerConfig({ + monitored_stats_required_freshness: 1000, + monitored_aggregated_stats_refresh_rate: 60000, + }) + ); + + const serviceStatus = getLatest(serviceStatus$); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments({}, {}, ['ok', 'internalError']); + + await sleep(0); + + stats$.next( + mockHealthStats({ + last_update: new Date(Date.now() - 1500).toISOString(), + }) + ); + + expect(await handler(context, req, res)).toMatchObject({ + body: { + status: 'error', + ...summarizeMonitoringStats( + mockHealthStats({ + last_update: expect.any(String), + stats: { + configuration: { + timestamp: expect.any(String), + }, + workload: { + timestamp: expect.any(String), + }, + runtime: { + timestamp: expect.any(String), + value: { + polling: { + last_successful_poll: expect.any(String), + }, + }, + }, + }, + }), + getTaskManagerConfig({}) + ), + }, + }); + + expect(await serviceStatus).toMatchObject({ + level: ServiceStatusLevels.unavailable, + summary: 'Task Manager is unavailable', + meta: { + status: 'error', + ...summarizeMonitoringStats( + mockHealthStats({ + last_update: expect.any(String), + stats: { + configuration: { + timestamp: expect.any(String), + }, + workload: { + timestamp: expect.any(String), + }, + runtime: { + timestamp: expect.any(String), + value: { + polling: { + last_successful_poll: expect.any(String), + }, + }, + }, + }, + }), + getTaskManagerConfig({}) + ), + }, + }); + }); + + it('returns a error status if the workload stats have not been updated within the required cold freshness', async () => { + const router = httpServiceMock.createRouter(); + + const stats$ = new Subject(); + + healthRoute( + router, + stats$, + loggingSystemMock.create().get(), + uuid.v4(), + getTaskManagerConfig({ + monitored_stats_required_freshness: 5000, + monitored_aggregated_stats_refresh_rate: 60000, + }) + ); + + await sleep(0); + + const lastUpdateOfWorkload = new Date(Date.now() - 120000).toISOString(); + stats$.next( + mockHealthStats({ + stats: { + workload: { + timestamp: lastUpdateOfWorkload, + }, + }, + }) + ); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments({}, {}, ['ok', 'internalError']); + + await sleep(2000); + + expect(await handler(context, req, res)).toMatchObject({ + body: { + status: 'error', + ...summarizeMonitoringStats( + mockHealthStats({ + last_update: expect.any(String), + stats: { + configuration: { + timestamp: expect.any(String), + }, + workload: { + timestamp: expect.any(String), + }, + runtime: { + timestamp: expect.any(String), + value: { + polling: { + last_successful_poll: expect.any(String), + }, + }, + }, + }, + }), + getTaskManagerConfig() + ), + }, + }); + }); + + it('returns a error status if the poller hasnt polled within the required hot freshness', async () => { + const router = httpServiceMock.createRouter(); + + const stats$ = new Subject(); + healthRoute( + router, + stats$, + loggingSystemMock.create().get(), + uuid.v4(), + getTaskManagerConfig({ + monitored_stats_required_freshness: 1000, + monitored_aggregated_stats_refresh_rate: 60000, + }) + ); + + await sleep(0); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const last_successful_poll = new Date(Date.now() - 2000).toISOString(); + stats$.next( + mockHealthStats({ + stats: { + runtime: { + value: { + polling: { + last_successful_poll, + }, + }, + }, + }, + }) + ); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments({}, {}, ['ok', 'internalError']); + + expect(await handler(context, req, res)).toMatchObject({ + body: { + status: 'error', + ...summarizeMonitoringStats( + mockHealthStats({ + last_update: expect.any(String), + stats: { + configuration: { + timestamp: expect.any(String), + }, + workload: { + timestamp: expect.any(String), + }, + runtime: { + timestamp: expect.any(String), + value: { + polling: { + last_successful_poll, + }, + }, + }, + }, + }), + getTaskManagerConfig() + ), + }, + }); + }); +}); + +function mockHealthStats(overrides = {}) { + return (merge( + { + last_update: new Date().toISOString(), + stats: { + configuration: { + timestamp: new Date().toISOString(), + value: { + value: { + max_workers: 10, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, + }, + }, + }, + }, + workload: { + timestamp: new Date().toISOString(), + value: { + count: 4, + taskTypes: { + actions_telemetry: { count: 2, status: { idle: 2 } }, + alerting_telemetry: { count: 1, status: { idle: 1 } }, + session_cleanup: { count: 1, status: { idle: 1 } }, + }, + schedule: {}, + overdue: 0, + estimatedScheduleDensity: [], + }, + }, + runtime: { + timestamp: new Date().toISOString(), + value: { + drift: [1000, 60000], + execution: { + duration: [], + result_frequency_percent_as_number: [], + }, + polling: { + last_successful_poll: new Date().toISOString(), + result_frequency_percent_as_number: [ + 'NoTasksClaimed', + 'NoTasksClaimed', + 'NoTasksClaimed', + ], + }, + }, + }, + }, + }, + overrides + ) as unknown) as MonitoringStats; +} + +async function getLatest(stream$: Observable) { + return new Promise((resolve) => stream$.pipe(take(1)).subscribe((stats) => resolve(stats))); +} + +const getTaskManagerConfig = (overrides: Partial = {}) => + configSchema.validate( + overrides.monitored_stats_required_freshness + ? { + // use `monitored_stats_required_freshness` as poll interval otherwise we might + // fail validation as it must be greather than the poll interval + poll_interval: overrides.monitored_stats_required_freshness, + ...overrides, + } + : overrides + ); diff --git a/x-pack/plugins/task_manager/server/routes/health.ts b/x-pack/plugins/task_manager/server/routes/health.ts new file mode 100644 index 0000000000000..8ddd728063d2a --- /dev/null +++ b/x-pack/plugins/task_manager/server/routes/health.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'kibana/server'; +import { Observable, Subject } from 'rxjs'; +import { tap, map } from 'rxjs/operators'; +import { throttleTime } from 'rxjs/operators'; +import { isString } from 'lodash'; +import { JsonValue } from 'src/plugins/kibana_utils/common'; +import { Logger, ServiceStatus, ServiceStatusLevels } from '../../../../../src/core/server'; +import { + MonitoringStats, + summarizeMonitoringStats, + HealthStatus, + RawMonitoringStats, +} from '../monitoring'; +import { TaskManagerConfig } from '../config'; + +type MonitoredHealth = RawMonitoringStats & { id: string; status: HealthStatus; timestamp: string }; + +const LEVEL_SUMMARY = { + [ServiceStatusLevels.available.toString()]: 'Task Manager is healthy', + [ServiceStatusLevels.degraded.toString()]: 'Task Manager is unhealthy', + [ServiceStatusLevels.unavailable.toString()]: 'Task Manager is unavailable', +}; + +export function healthRoute( + router: IRouter, + monitoringStats$: Observable, + logger: Logger, + taskManagerId: string, + config: TaskManagerConfig +): Observable { + // if "hot" health stats are any more stale than monitored_stats_required_freshness (pollInterval +1s buffer by default) + // consider the system unhealthy + const requiredHotStatsFreshness: number = config.monitored_stats_required_freshness; + + // if "cold" health stats are any more stale than the configured refresh (+ a buffer), consider the system unhealthy + const requiredColdStatsFreshness: number = config.monitored_aggregated_stats_refresh_rate * 1.5; + + function calculateStatus(monitoredStats: MonitoringStats): MonitoredHealth { + const now = Date.now(); + const timestamp = new Date(now).toISOString(); + const summarizedStats = summarizeMonitoringStats(monitoredStats, config); + + /** + * If the monitored stats aren't fresh, return a red status + */ + const healthStatus = + hasStatus(summarizedStats.stats, HealthStatus.Error) || + hasExpiredHotTimestamps(summarizedStats, now, requiredHotStatsFreshness) || + hasExpiredColdTimestamps(summarizedStats, now, requiredColdStatsFreshness) + ? HealthStatus.Error + : hasStatus(summarizedStats.stats, HealthStatus.Warning) + ? HealthStatus.Warning + : HealthStatus.OK; + return { id: taskManagerId, timestamp, status: healthStatus, ...summarizedStats }; + } + + const serviceStatus$: Subject = new Subject(); + + /* keep track of last health summary, as we'll return that to the next call to _health */ + let lastMonitoredStats: MonitoringStats | null = null; + + /* Log Task Manager stats as a Debug log line at a fixed interval */ + monitoringStats$ + .pipe( + throttleTime(requiredHotStatsFreshness), + tap((stats) => { + lastMonitoredStats = stats; + }), + // Only calculate the summerized stats (calculates all runnign averages and evaluates state) + // when needed by throttling down to the requiredHotStatsFreshness + map((stats) => withServiceStatus(calculateStatus(stats))) + ) + .subscribe(([monitoredHealth, serviceStatus]) => { + serviceStatus$.next(serviceStatus); + logger.debug(`Latest Monitored Stats: ${JSON.stringify(monitoredHealth)}`); + }); + + router.get( + { + path: '/api/task_manager/_health', + validate: false, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise { + return res.ok({ + body: lastMonitoredStats + ? calculateStatus(lastMonitoredStats) + : { id: taskManagerId, timestamp: new Date().toISOString(), status: HealthStatus.Error }, + }); + } + ); + return serviceStatus$; +} + +export function withServiceStatus( + monitoredHealth: MonitoredHealth +): [MonitoredHealth, ServiceStatus] { + const level = + monitoredHealth.status === HealthStatus.OK + ? ServiceStatusLevels.available + : monitoredHealth.status === HealthStatus.Warning + ? ServiceStatusLevels.degraded + : ServiceStatusLevels.unavailable; + return [ + monitoredHealth, + { + level, + summary: LEVEL_SUMMARY[level.toString()], + meta: monitoredHealth, + }, + ]; +} + +/** + * If certain "hot" stats are not fresh, then the _health api will should return a Red status + * @param monitoringStats The monitored stats + * @param now The time to compare against + * @param requiredFreshness How fresh should these stats be + */ +function hasExpiredHotTimestamps( + monitoringStats: RawMonitoringStats, + now: number, + requiredFreshness: number +): boolean { + return ( + now - + getOldestTimestamp( + monitoringStats.last_update, + monitoringStats.stats.runtime?.value.polling.last_successful_poll + ) > + requiredFreshness + ); +} + +function hasExpiredColdTimestamps( + monitoringStats: RawMonitoringStats, + now: number, + requiredFreshness: number +): boolean { + return now - getOldestTimestamp(monitoringStats.stats.workload?.timestamp) > requiredFreshness; +} + +function hasStatus(stats: RawMonitoringStats['stats'], status: HealthStatus): boolean { + return Object.values(stats) + .map((stat) => stat?.status === status) + .includes(true); +} + +function getOldestTimestamp(...timestamps: Array): number { + const validTimestamps = timestamps + .map((timestamp) => (isString(timestamp) ? Date.parse(timestamp) : NaN)) + .filter((timestamp) => !isNaN(timestamp)); + return validTimestamps.length ? Math.min(...validTimestamps) : 0; +} diff --git a/x-pack/plugins/task_manager/server/routes/index.ts b/x-pack/plugins/task_manager/server/routes/index.ts new file mode 100644 index 0000000000000..4fa1aa6cb7a9b --- /dev/null +++ b/x-pack/plugins/task_manager/server/routes/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { healthRoute } from './health'; diff --git a/x-pack/plugins/task_manager/server/task_events.ts b/x-pack/plugins/task_manager/server/task_events.ts index e1dd85f868cdd..b011d435e28dc 100644 --- a/x-pack/plugins/task_manager/server/task_events.ts +++ b/x-pack/plugins/task_manager/server/task_events.ts @@ -9,63 +9,109 @@ import { Option } from 'fp-ts/lib/Option'; import { ConcreteTaskInstance } from './task'; import { Result, Err } from './lib/result_type'; +import { FillPoolResult } from './lib/fill_pool'; +import { PollingError } from './polling'; +import { TaskRunResult } from './task_runner'; export enum TaskEventType { TASK_CLAIM = 'TASK_CLAIM', TASK_MARK_RUNNING = 'TASK_MARK_RUNNING', TASK_RUN = 'TASK_RUN', TASK_RUN_REQUEST = 'TASK_RUN_REQUEST', + TASK_POLLING_CYCLE = 'TASK_POLLING_CYCLE', +} + +export interface TaskTiming { + start: number; + stop: number; +} + +export function startTaskTimer(): () => TaskTiming { + const start = Date.now(); + return () => ({ start, stop: Date.now() }); } export interface TaskEvent { - id: string; + id?: string; + timing?: TaskTiming; type: TaskEventType; event: Result; } +export interface RanTask { + task: ConcreteTaskInstance; + result: TaskRunResult; +} +export type ErroredTask = RanTask & { + error: Error; +}; + export type TaskMarkRunning = TaskEvent; -export type TaskRun = TaskEvent; +export type TaskRun = TaskEvent; export type TaskClaim = TaskEvent>; export type TaskRunRequest = TaskEvent; +export type TaskPollingCycle = TaskEvent>; export function asTaskMarkRunningEvent( id: string, - event: Result + event: Result, + timing?: TaskTiming ): TaskMarkRunning { return { id, type: TaskEventType.TASK_MARK_RUNNING, event, + timing, }; } -export function asTaskRunEvent(id: string, event: Result): TaskRun { +export function asTaskRunEvent( + id: string, + event: Result, + timing?: TaskTiming +): TaskRun { return { id, type: TaskEventType.TASK_RUN, event, + timing, }; } export function asTaskClaimEvent( id: string, - event: Result> + event: Result>, + timing?: TaskTiming ): TaskClaim { return { id, type: TaskEventType.TASK_CLAIM, event, + timing, }; } export function asTaskRunRequestEvent( id: string, // we only emit a TaskRunRequest event when it fails - event: Err + event: Err, + timing?: TaskTiming ): TaskRunRequest { return { id, type: TaskEventType.TASK_RUN_REQUEST, event, + timing, + }; +} + +export function asTaskPollingCycleEvent( + event: Result>, + timing?: TaskTiming +): TaskPollingCycle { + return { + type: TaskEventType.TASK_POLLING_CYCLE, + event, + timing, }; } @@ -85,3 +131,8 @@ export function isTaskRunRequestEvent( ): taskEvent is TaskRunRequest { return taskEvent.type === TaskEventType.TASK_RUN_REQUEST; } +export function isTaskPollingCycleEvent( + taskEvent: TaskEvent +): taskEvent is TaskPollingCycle { + return taskEvent.type === TaskEventType.TASK_POLLING_CYCLE; +} diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index 12b731b2b78ae..a174af71ef18f 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -7,7 +7,9 @@ import sinon from 'sinon'; import { of, Subject } from 'rxjs'; import { TaskPool, TaskPoolRunResult } from './task_pool'; -import { mockLogger, resolvable, sleep } from './test_utils'; +import { resolvable, sleep } from './test_utils'; +import { loggingSystemMock } from '../../../../src/core/server/mocks'; +import { Logger } from '../../../../src/core/server'; import { asOk } from './lib/result_type'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; import moment from 'moment'; @@ -16,7 +18,7 @@ describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { const pool = new TaskPool({ maxWorkers$: of(200), - logger: mockLogger(), + logger: loggingSystemMock.create().get(), }); const result = await pool.run([{ ...mockTask() }, { ...mockTask() }, { ...mockTask() }]); @@ -28,7 +30,7 @@ describe('TaskPool', () => { test('availableWorkers are a function of total_capacity - occupiedWorkers', async () => { const pool = new TaskPool({ maxWorkers$: of(10), - logger: mockLogger(), + logger: loggingSystemMock.create().get(), }); const result = await pool.run([{ ...mockTask() }, { ...mockTask() }, { ...mockTask() }]); @@ -41,7 +43,7 @@ describe('TaskPool', () => { const maxWorkers$ = new Subject(); const pool = new TaskPool({ maxWorkers$, - logger: mockLogger(), + logger: loggingSystemMock.create().get(), }); expect(pool.availableWorkers).toEqual(0); @@ -52,7 +54,7 @@ describe('TaskPool', () => { test('does not run tasks that are beyond its available capacity', async () => { const pool = new TaskPool({ maxWorkers$: of(2), - logger: mockLogger(), + logger: loggingSystemMock.create().get(), }); const shouldRun = mockRun(); @@ -71,7 +73,7 @@ describe('TaskPool', () => { }); test('should log when marking a Task as running fails', async () => { - const logger = mockLogger(); + const logger = loggingSystemMock.create().get(); const pool = new TaskPool({ maxWorkers$: of(2), logger, @@ -84,7 +86,7 @@ describe('TaskPool', () => { const result = await pool.run([mockTask(), taskFailedToMarkAsRunning, mockTask()]); - expect(logger.error.mock.calls[0]).toMatchInlineSnapshot(` + expect((logger as jest.Mocked).error.mock.calls[0]).toMatchInlineSnapshot(` Array [ "Failed to mark Task TaskType \\"shooooo\\" as running: Mark Task as running has failed miserably", ] @@ -94,7 +96,7 @@ describe('TaskPool', () => { }); test('should log when running a Task fails', async () => { - const logger = mockLogger(); + const logger = loggingSystemMock.create().get(); const pool = new TaskPool({ maxWorkers$: of(3), logger, @@ -107,7 +109,7 @@ describe('TaskPool', () => { const result = await pool.run([mockTask(), taskFailedToRun, mockTask()]); - expect(logger.warn.mock.calls[0]).toMatchInlineSnapshot(` + expect((logger as jest.Mocked).warn.mock.calls[0]).toMatchInlineSnapshot(` Array [ "Task TaskType \\"shooooo\\" failed in attempt to run: Run Task has failed miserably", ] @@ -117,7 +119,7 @@ describe('TaskPool', () => { }); test('should not log when running a Task fails due to the Task SO having been deleted while in flight', async () => { - const logger = mockLogger(); + const logger = loggingSystemMock.create().get(); const pool = new TaskPool({ maxWorkers$: of(3), logger, @@ -139,7 +141,7 @@ describe('TaskPool', () => { }); test('Running a task which fails still takes up capacity', async () => { - const logger = mockLogger(); + const logger = loggingSystemMock.create().get(); const pool = new TaskPool({ maxWorkers$: of(1), logger, @@ -159,7 +161,7 @@ describe('TaskPool', () => { test('clears up capacity when a task completes', async () => { const pool = new TaskPool({ maxWorkers$: of(1), - logger: mockLogger(), + logger: loggingSystemMock.create().get(), }); const firstWork = resolvable(); @@ -202,7 +204,7 @@ describe('TaskPool', () => { }); test('run cancels expired tasks prior to running new tasks', async () => { - const logger = mockLogger(); + const logger = loggingSystemMock.create().get(); const pool = new TaskPool({ maxWorkers$: of(2), logger, @@ -259,7 +261,7 @@ describe('TaskPool', () => { }); test('logs if cancellation errors', async () => { - const logger = mockLogger(); + const logger = loggingSystemMock.create().get(); const pool = new TaskPool({ logger, maxWorkers$: of(20), @@ -290,7 +292,7 @@ describe('TaskPool', () => { // Allow the task to cancel... await cancelled; - expect(logger.error.mock.calls[0][0]).toMatchInlineSnapshot( + expect((logger as jest.Mocked).error.mock.calls[0][0]).toMatchInlineSnapshot( `"Failed to cancel task \\"shooooo!\\": Error: Dern!"` ); }); diff --git a/x-pack/plugins/task_manager/server/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_runner.test.ts index 8fb1df444c603..676eeedf08f5a 100644 --- a/x-pack/plugins/task_manager/server/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_runner.test.ts @@ -6,16 +6,18 @@ import _ from 'lodash'; import sinon from 'sinon'; -import { minutesFromNow } from './lib/intervals'; +import { secondsFromNow } from './lib/intervals'; import { asOk, asErr } from './lib/result_type'; -import { TaskEvent, asTaskRunEvent, asTaskMarkRunningEvent } from './task_events'; +import { TaskManagerRunner, TaskRunResult } from './task_runner'; +import { TaskEvent, asTaskRunEvent, asTaskMarkRunningEvent, TaskRun } from './task_events'; import { ConcreteTaskInstance, TaskStatus, TaskDefinition, RunResult } from './task'; -import { TaskManagerRunner } from './task_runner'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; import moment from 'moment'; import { TaskTypeDictionary } from './task_type_dictionary'; import { mockLogger } from './test_utils'; +const minutesFromNow = (mins: number): Date => secondsFromNow(mins * 60); + let fakeTimer: sinon.SinonFakeTimers; beforeAll(() => { @@ -812,7 +814,9 @@ describe('TaskManagerRunner', () => { await runner.run(); - expect(onTaskEvent).toHaveBeenCalledWith(asTaskRunEvent(id, asOk(instance))); + expect(onTaskEvent).toHaveBeenCalledWith( + withAnyTiming(asTaskRunEvent(id, asOk({ task: instance, result: TaskRunResult.Success }))) + ); }); test('emits TaskEvent when a recurring task is run successfully', async () => { @@ -839,14 +843,16 @@ describe('TaskManagerRunner', () => { await runner.run(); - expect(onTaskEvent).toHaveBeenCalledWith(asTaskRunEvent(id, asOk(instance))); + expect(onTaskEvent).toHaveBeenCalledWith( + withAnyTiming(asTaskRunEvent(id, asOk({ task: instance, result: TaskRunResult.Success }))) + ); }); test('emits TaskEvent when a task run throws an error', async () => { const id = _.random(1, 20).toString(); const error = new Error('Dangit!'); const onTaskEvent = jest.fn(); - const { runner } = testOpts({ + const { runner, instance } = testOpts({ onTaskEvent, instance: { id, @@ -864,7 +870,11 @@ describe('TaskManagerRunner', () => { }); await runner.run(); - expect(onTaskEvent).toHaveBeenCalledWith(asTaskRunEvent(id, asErr(error))); + expect(onTaskEvent).toHaveBeenCalledWith( + withAnyTiming( + asTaskRunEvent(id, asErr({ error, task: instance, result: TaskRunResult.RetryScheduled })) + ) + ); expect(onTaskEvent).toHaveBeenCalledTimes(1); }); @@ -872,7 +882,7 @@ describe('TaskManagerRunner', () => { const id = _.random(1, 20).toString(); const error = new Error('Dangit!'); const onTaskEvent = jest.fn(); - const { runner } = testOpts({ + const { runner, instance } = testOpts({ onTaskEvent, instance: { id, @@ -893,7 +903,11 @@ describe('TaskManagerRunner', () => { await runner.run(); - expect(onTaskEvent).toHaveBeenCalledWith(asTaskRunEvent(id, asErr(error))); + expect(onTaskEvent).toHaveBeenCalledWith( + withAnyTiming( + asTaskRunEvent(id, asErr({ error, task: instance, result: TaskRunResult.RetryScheduled })) + ) + ); expect(onTaskEvent).toHaveBeenCalledTimes(1); }); @@ -901,7 +915,7 @@ describe('TaskManagerRunner', () => { const id = _.random(1, 20).toString(); const error = new Error('Dangit!'); const onTaskEvent = jest.fn(); - const { runner, store } = testOpts({ + const { runner, store, instance: originalInstance } = testOpts({ onTaskEvent, instance: { id, @@ -925,7 +939,18 @@ describe('TaskManagerRunner', () => { const instance = store.update.args[0][0]; expect(instance.status).toBe('failed'); - expect(onTaskEvent).toHaveBeenCalledWith(asTaskRunEvent(id, asErr(error))); + expect(onTaskEvent).toHaveBeenCalledWith( + withAnyTiming( + asTaskRunEvent( + id, + asErr({ + error, + task: originalInstance, + result: TaskRunResult.Failed, + }) + ) + ) + ); expect(onTaskEvent).toHaveBeenCalledTimes(1); }); }); @@ -936,6 +961,13 @@ describe('TaskManagerRunner', () => { onTaskEvent?: (event: TaskEvent) => void; } + function withAnyTiming(taskRun: TaskRun) { + return { + ...taskRun, + timing: { start: expect.any(Number), stop: expect.any(Number) }, + }; + } + function testOpts(opts: TestOpts) { const callCluster = sinon.stub(); const createTaskRunner = sinon.stub(); diff --git a/x-pack/plugins/task_manager/server/task_runner.ts b/x-pack/plugins/task_manager/server/task_runner.ts index 24a487e366029..45e4cb3000570 100644 --- a/x-pack/plugins/task_manager/server/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_runner.ts @@ -10,16 +10,23 @@ * rescheduling, middleware application, etc. */ +import { Logger } from 'src/core/server'; import apm from 'elastic-apm-node'; import { performance } from 'perf_hooks'; import Joi from 'joi'; import { identity, defaults, flow } from 'lodash'; -import { Logger } from '../../../../src/core/server'; -import { asOk, asErr, mapErr, eitherAsync, unwrap, mapOk, Result } from './lib/result_type'; -import { TaskRun, TaskMarkRunning, asTaskRunEvent, asTaskMarkRunningEvent } from './task_events'; -import { intervalFromDate, intervalFromNow } from './lib/intervals'; import { Middleware } from './lib/middleware'; +import { asOk, asErr, mapErr, eitherAsync, unwrap, isOk, mapOk, Result } from './lib/result_type'; +import { + TaskRun, + TaskMarkRunning, + asTaskRunEvent, + asTaskMarkRunningEvent, + startTaskTimer, + TaskTiming, +} from './task_events'; +import { intervalFromDate, intervalFromNow } from './lib/intervals'; import { CancelFunction, CancellableTask, @@ -63,6 +70,21 @@ type Opts = { onTaskEvent?: (event: TaskRun | TaskMarkRunning) => void; } & Pick; +export enum TaskRunResult { + // Task completed successfully + Success = 'Success', + // Recurring Task completed successfully + SuccessRescheduled = 'Success', + // // Task completed successfully after a retry + // SuccessfulRetry = 'SuccessfulRetry', + // // Recurring Task completed successfully after a retry + // SuccessfulRetryRescheduled = 'SuccessfulRetry', + // Task has failed and a retry has been scheduled + RetryScheduled = 'RetryScheduled', + // Task has failed + Failed = 'Failed', +} + /** * Runs a background task, ensures that errors are properly handled, * allows for cancellation. @@ -172,6 +194,7 @@ export class TaskManagerRunner implements TaskRunner { taskInstance: this.instance, }); + const stopTaskTimer = startTaskTimer(); const apmTrans = apm.startTransaction( `taskManager run ${this.instance.taskType}`, 'taskManager' @@ -181,13 +204,16 @@ export class TaskManagerRunner implements TaskRunner { const result = await this.task.run(); const validatedResult = this.validateResult(result); if (apmTrans) apmTrans.end('success'); - return this.processResult(validatedResult); + return this.processResult(validatedResult, stopTaskTimer()); } catch (err) { this.logger.error(`Task ${this} failed: ${err}`); // in error scenario, we can not get the RunResult // re-use modifiedContext's state, which is correct as of beforeRun if (apmTrans) apmTrans.end('error'); - return this.processResult(asErr({ error: err, state: modifiedContext.taskInstance.state })); + return this.processResult( + asErr({ error: err, state: modifiedContext.taskInstance.state }), + stopTaskTimer() + ); } } @@ -337,8 +363,9 @@ export class TaskManagerRunner implements TaskRunner { private async processResultForRecurringTask( result: Result - ): Promise { - const fieldUpdates = flow( + ): Promise { + const hasTaskRunFailed = isOk(result); + const fieldUpdates: Partial & Pick = flow( // if running the task has failed ,try to correct by scheduling a retry in the near future mapErr(this.rescheduleFailedRun), // if retrying is possible (new runAt) or this is an recurring task - reschedule @@ -357,7 +384,7 @@ export class TaskManagerRunner implements TaskRunner { await this.bufferedTaskStore.update( defaults( { - ...(fieldUpdates as Partial), + ...fieldUpdates, // reset fields that track the lifecycle of the concluded `task run` startedAt: null, retryAt: null, @@ -366,9 +393,15 @@ export class TaskManagerRunner implements TaskRunner { this.instance ) ); + + return fieldUpdates.status === TaskStatus.Failed + ? TaskRunResult.Failed + : hasTaskRunFailed + ? TaskRunResult.SuccessRescheduled + : TaskRunResult.RetryScheduled; } - private async processResultWhenDone(): Promise { + private async processResultWhenDone(): Promise { // not a recurring task: clean up by removing the task instance from store try { await this.bufferedTaskStore.remove(this.instance.id); @@ -379,24 +412,38 @@ export class TaskManagerRunner implements TaskRunner { throw err; } } + return TaskRunResult.Success; } private async processResult( - result: Result + result: Result, + taskTiming: TaskTiming ): Promise> { + const task = this.instance; await eitherAsync( result, async ({ runAt }: SuccessfulRunResult) => { - if (runAt || this.instance.schedule) { - await this.processResultForRecurringTask(result); - } else { - await this.processResultWhenDone(); - } - this.onTaskEvent(asTaskRunEvent(this.id, asOk(this.instance))); + this.onTaskEvent( + asTaskRunEvent( + this.id, + asOk({ + task, + result: await (runAt || task.schedule + ? this.processResultForRecurringTask(result) + : this.processResultWhenDone()), + }), + taskTiming + ) + ); }, async ({ error }: FailedRunResult) => { - await this.processResultForRecurringTask(result); - this.onTaskEvent(asTaskRunEvent(this.id, asErr(error))); + this.onTaskEvent( + asTaskRunEvent( + this.id, + asErr({ task, result: await this.processResultForRecurringTask(result), error }), + taskTiming + ) + ); } ); return result; diff --git a/x-pack/plugins/task_manager/server/task_scheduling.test.ts b/x-pack/plugins/task_manager/server/task_scheduling.test.ts index 1f7f9250d9014..8d660f57ab875 100644 --- a/x-pack/plugins/task_manager/server/task_scheduling.test.ts +++ b/x-pack/plugins/task_manager/server/task_scheduling.test.ts @@ -21,6 +21,7 @@ import { asErr, asOk } from './lib/result_type'; import { ConcreteTaskInstance, TaskLifecycleResult, TaskStatus } from './task'; import { createInitialMiddleware } from './lib/middleware'; import { taskStoreMock } from './task_store.mock'; +import { TaskRunResult } from './task_runner'; import { mockLogger } from './test_utils'; describe('TaskScheduling', () => { @@ -113,7 +114,7 @@ describe('TaskScheduling', () => { const result = taskScheduling.runNow(id); const task = { id } as ConcreteTaskInstance; - events$.next(asTaskRunEvent(id, asOk(task))); + events$.next(asTaskRunEvent(id, asOk({ task, result: TaskRunResult.Success }))); return expect(result).resolves.toEqual({ id }); }); @@ -132,7 +133,16 @@ describe('TaskScheduling', () => { const task = { id } as ConcreteTaskInstance; events$.next(asTaskClaimEvent(id, asOk(task))); events$.next(asTaskMarkRunningEvent(id, asOk(task))); - events$.next(asTaskRunEvent(id, asErr(new Error('some thing gone wrong')))); + events$.next( + asTaskRunEvent( + id, + asErr({ + task, + error: new Error('some thing gone wrong'), + result: TaskRunResult.Failed, + }) + ) + ); return expect(result).rejects.toMatchInlineSnapshot( `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2": Error: some thing gone wrong]` @@ -306,10 +316,20 @@ describe('TaskScheduling', () => { const otherTask = { id: differentTask } as ConcreteTaskInstance; events$.next(asTaskClaimEvent(id, asOk(task))); events$.next(asTaskClaimEvent(differentTask, asOk(otherTask))); + events$.next( + asTaskRunEvent(differentTask, asOk({ task: otherTask, result: TaskRunResult.Success })) + ); - events$.next(asTaskRunEvent(differentTask, asOk(task))); - - events$.next(asTaskRunEvent(id, asErr(new Error('some thing gone wrong')))); + events$.next( + asTaskRunEvent( + id, + asErr({ + task, + error: new Error('some thing gone wrong'), + result: TaskRunResult.Failed, + }) + ) + ); return expect(result).rejects.toMatchInlineSnapshot( `[Error: Failed to run task "01ddff11-e88a-4d13-bc4e-256164e755e2": Error: some thing gone wrong]` diff --git a/x-pack/plugins/task_manager/server/task_scheduling.ts b/x-pack/plugins/task_manager/server/task_scheduling.ts index 00f7d853d7114..9806ada386e4a 100644 --- a/x-pack/plugins/task_manager/server/task_scheduling.ts +++ b/x-pack/plugins/task_manager/server/task_scheduling.ts @@ -10,7 +10,13 @@ import { Option, map as mapOptional, getOrElse } from 'fp-ts/lib/Option'; import { Logger } from '../../../../src/core/server'; import { asOk, either, map, mapErr, promiseResult } from './lib/result_type'; -import { isTaskRunEvent, isTaskClaimEvent, isTaskRunRequestEvent } from './task_events'; +import { + isTaskRunEvent, + isTaskClaimEvent, + isTaskRunRequestEvent, + RanTask, + ErroredTask, +} from './task_events'; import { Middleware } from './lib/middleware'; import { ConcreteTaskInstance, @@ -23,6 +29,7 @@ import { import { TaskStore } from './task_store'; import { ensureDeprecatedFieldsAreCorrected } from './lib/correct_deprecated_fields'; import { TaskLifecycleEvent, TaskPollingLifecycle } from './polling_lifecycle'; +import { FillPoolResult } from './lib/fill_pool'; const VERSION_CONFLICT_STATUS = 409; @@ -118,16 +125,19 @@ export class TaskScheduling { return reject(await this.identifyTaskFailureReason(taskId, error)); }, taskEvent.event); } else { - either>( + either< + RanTask | ConcreteTaskInstance | FillPoolResult, + Error | ErroredTask | Option + >( taskEvent.event, - (taskInstance: ConcreteTaskInstance) => { + (taskInstance: RanTask | ConcreteTaskInstance | FillPoolResult) => { // resolve if the task has run sucessfully if (isTaskRunEvent(taskEvent)) { subscription.unsubscribe(); - resolve({ id: taskInstance.id }); + resolve({ id: (taskInstance as RanTask).task.id }); } }, - async (error: Error | Option) => { + async (errorResult: Error | ErroredTask | Option) => { // reject if any error event takes place for the requested task subscription.unsubscribe(); return reject( @@ -135,7 +145,9 @@ export class TaskScheduling { `Failed to run task "${taskId}": ${ isTaskRunRequestEvent(taskEvent) ? `Task Manager is at capacity, please try again later` - : error + : isTaskRunEvent(taskEvent) + ? `${(errorResult as ErroredTask).error}` + : `${errorResult}` }` ) ); diff --git a/x-pack/plugins/task_manager/server/task_store.mock.ts b/x-pack/plugins/task_manager/server/task_store.mock.ts index 9b82a3e3ee7ab..7af1b9ef5f110 100644 --- a/x-pack/plugins/task_manager/server/task_store.mock.ts +++ b/x-pack/plugins/task_manager/server/task_store.mock.ts @@ -31,6 +31,7 @@ export const taskStoreMock = { get: jest.fn(), getLifecycle: jest.fn(), fetch: jest.fn(), + aggregate: jest.fn(), maxAttempts, index, taskManagerId, diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index a40df3b84132e..46e55df4ee1e6 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -415,41 +415,6 @@ describe('TaskStore', () => { ], }, }, - { - bool: { - should: [ - { exists: { field: 'task.schedule' } }, - { - bool: { - must: [ - { term: { 'task.taskType': 'foo' } }, - { - range: { - 'task.attempts': { - lt: maxAttempts, - }, - }, - }, - ], - }, - }, - { - bool: { - must: [ - { term: { 'task.taskType': 'bar' } }, - { - range: { - 'task.attempts': { - lt: customMaxAttempts, - }, - }, - }, - ], - }, - }, - ], - }, - }, ], }, }, @@ -501,6 +466,11 @@ if (doc['task.runAt'].size()!=0) { const maxAttempts = _.random(2, 43); const customMaxAttempts = _.random(44, 100); const definitions = new TaskTypeDictionary(mockLogger()); + const taskManagerId = uuid.v1(); + const fieldUpdates = { + ownerId: taskManagerId, + retryAt: new Date(Date.now()), + }; definitions.registerTaskDefinitions({ foo: { title: 'foo', @@ -514,10 +484,11 @@ if (doc['task.runAt'].size()!=0) { }); const { args: { - updateByQuery: { body: { query, sort } = {} }, + updateByQuery: { body: { query, script, sort } = {} }, }, } = await testClaimAvailableTasks({ opts: { + taskManagerId, maxAttempts, definitions, }, @@ -576,41 +547,6 @@ if (doc['task.runAt'].size()!=0) { ], }, }, - { - bool: { - should: [ - { exists: { field: 'task.schedule' } }, - { - bool: { - must: [ - { term: { 'task.taskType': 'foo' } }, - { - range: { - 'task.attempts': { - lt: maxAttempts, - }, - }, - }, - ], - }, - }, - { - bool: { - must: [ - { term: { 'task.taskType': 'bar' } }, - { - range: { - 'task.attempts': { - lt: customMaxAttempts, - }, - }, - }, - ], - }, - }, - ], - }, - }, ], }, }, @@ -640,6 +576,30 @@ if (doc['task.runAt'].size()!=0) { }, }); + expect(script).toMatchObject({ + source: ` + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } + `, + lang: 'painless', + params: { + fieldUpdates, + claimTasksById: [ + 'task:33c6977a-ed6d-43bd-98d9-3f827f7b7cd8', + 'task:a208b22c-14ec-4fb4-995f-d2ff7a3b03b8', + ], + taskMaxAttempts: { + bar: customMaxAttempts, + foo: maxAttempts, + }, + }, + }); + expect(sort).toMatchObject([ '_score', { @@ -665,6 +625,10 @@ if (doc['task.runAt'].size()!=0) { test('it claims tasks by setting their ownerId, status and retryAt', async () => { const taskManagerId = uuid.v1(); const claimOwnershipUntil = new Date(Date.now()); + const fieldUpdates = { + ownerId: taskManagerId, + retryAt: claimOwnershipUntil, + }; const { args: { updateByQuery: { body: { script } = {} }, @@ -679,12 +643,24 @@ if (doc['task.runAt'].size()!=0) { }, }); expect(script).toMatchObject({ - source: `ctx._source.task.ownerId=params.ownerId; ctx._source.task.status=params.status; ctx._source.task.retryAt=params.retryAt;`, + source: ` + if (ctx._source.task.schedule != null || ctx._source.task.attempts < params.taskMaxAttempts[ctx._source.task.taskType] || params.claimTasksById.contains(ctx._id)) { + ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) + .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) + .join(' ')} + } else { + ctx._source.task.status = "failed"; + } + `, lang: 'painless', params: { - ownerId: taskManagerId, - retryAt: claimOwnershipUntil, - status: 'claiming', + fieldUpdates, + claimTasksById: [], + taskMaxAttempts: { + dernstraight: 2, + report: 2, + yawn: 2, + }, }, }); }); diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 63b6ab7412ec5..8c0d7764e009f 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -41,26 +41,23 @@ import { shouldBeOneOf, mustBeAllOf, filterDownBy, - ExistsFilter, - TermFilter, - RangeFilter, asPinnedQuery, matchesClauses, SortOptions, } from './queries/query_clauses'; import { - updateFields, + updateFieldsAndMarkAsFailed, IdleTaskWithExpiredRunAt, InactiveTasks, RunningOrClaimingTaskWithExpiredRetryAt, - TaskWithSchedule, - taskWithLessThanMaxAttempts, SortByRunAtAndRetryAt, tasksClaimedByOwner, } from './queries/mark_available_tasks_as_claimed'; import { TaskTypeDictionary } from './task_type_dictionary'; +import { ESSearchResponse, ESSearchBody } from '../../apm/typings/elasticsearch'; + export interface StoreOpts { esClient: ElasticsearchClient; index: string; @@ -79,6 +76,9 @@ export interface SearchOpts { search_after?: unknown[]; } +export type AggregationOpts = Pick, 'aggs'> & + Pick; + export interface UpdateByQuerySearchOpts extends SearchOpts { script?: object; } @@ -259,18 +259,13 @@ export class TaskStore { claimTasksById: OwnershipClaimingOpts['claimTasksById'], size: OwnershipClaimingOpts['size'] ): Promise { - const tasksWithRemainingAttempts = [...this.definitions].map(([type, { maxAttempts }]) => - taskWithLessThanMaxAttempts(type, maxAttempts || this.maxAttempts) - ); + const taskMaxAttempts = [...this.definitions].reduce((accumulator, [type, { maxAttempts }]) => { + return { ...accumulator, [type]: maxAttempts || this.maxAttempts }; + }, {}); const queryForScheduledTasks = mustBeAllOf( // Either a task with idle status and runAt <= now or // status running or claiming with a retryAt <= now. - shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt), - // Either task has a schedule or the attempts < the maximum configured - shouldBeOneOf( - TaskWithSchedule, - ...tasksWithRemainingAttempts - ) + shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt) ); // The documents should be sorted by runAt/retryAt, unless there are pinned @@ -295,11 +290,14 @@ export class TaskStore { ), filterDownBy(InactiveTasks) ), - update: updateFields({ - ownerId: this.taskManagerId, - status: 'claiming', - retryAt: claimOwnershipUntil, - }), + update: updateFieldsAndMarkAsFailed( + { + ownerId: this.taskManagerId, + retryAt: claimOwnershipUntil, + }, + claimTasksById || [], + taskMaxAttempts + ), sort, }), { @@ -501,6 +499,25 @@ export class TaskStore { } } + public async aggregate({ + aggs, + query, + size = 0, + }: TSearchRequest): Promise> { + const { body } = await this.esClient.search< + ESSearchResponse + >({ + index: this.index, + ignore_unavailable: true, + body: ensureAggregationOnlyReturnsTaskObjects({ + query, + aggs, + size, + }), + }); + return body; + } + private async updateByQuery( opts: UpdateByQuerySearchOpts = {}, // eslint-disable-next-line @typescript-eslint/naming-convention @@ -586,6 +603,22 @@ function ensureQueryOnlyReturnsTaskObjects(opts: SearchOpts): SearchOpts { }; } +function ensureAggregationOnlyReturnsTaskObjects(opts: AggregationOpts): AggregationOpts { + const originalQuery = opts.query; + const filterToOnlyTasks = { + bool: { + filter: [{ term: { type: 'task' } }], + }, + }; + const query = originalQuery + ? { bool: { must: [filterToOnlyTasks, originalQuery] } } + : filterToOnlyTasks; + return { + ...opts, + query, + }; +} + function isSavedObjectsUpdateResponse( result: SavedObjectsUpdateResponse | Error ): result is SavedObjectsUpdateResponse { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 396a06205eaa9..c965623ebfc17 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1706,6 +1706,16 @@ "properties": { "overview": { "type": "long" + }, + "setup_guide": { + "type": "long" + } + } + }, + "ui_error": { + "properties": { + "cannot_connect": { + "type": "long" } } }, @@ -3240,7 +3250,7 @@ "type": "boolean" }, "authProviderCount": { - "type": "number" + "type": "long" }, "enabledAuthProviders": { "type": "array", diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 8f19db6fc827d..404f9aeec7c68 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -102,11 +102,6 @@ export const StepDefineForm: FC = React.memo((props) => { toastNotifications, }; - // TODO This should use the actual value of `indices.query.bool.max_clause_count` - const maxIndexFields = 1024; - const numIndexFields = indexPattern.fields.length; - const disabledQuery = numIndexFields > maxIndexFields; - const copyToClipboardSource = getIndexDevConsoleStatement(pivotQuery, indexPattern.title); const copyToClipboardSourceDescription = i18n.translate( 'xpack.transform.indexPreview.copyClipboardTooltip', @@ -181,18 +176,6 @@ export const StepDefineForm: FC = React.memo((props) => { label={i18n.translate('xpack.transform.stepDefineForm.indexPatternLabel', { defaultMessage: 'Index pattern', })} - helpText={ - disabledQuery - ? i18n.translate('xpack.transform.stepDefineForm.indexPatternHelpText', { - defaultMessage: - 'An optional query for this index pattern is not supported. The number of supported index fields is {maxIndexFields} whereas this index has {numIndexFields} fields.', - values: { - maxIndexFields, - numIndexFields, - }, - }) - : '' - } > {indexPattern.title} @@ -214,7 +197,7 @@ export const StepDefineForm: FC = React.memo((props) => { {/* Flex Column #1: Search Bar / Advanced Search Editor */} {searchItems.savedSearch === undefined && ( <> - {!disabledQuery && !isAdvancedSourceEditorEnabled && ( + {!isAdvancedSourceEditorEnabled && ( 阈值 0。", @@ -20280,18 +20266,14 @@ "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText": "时间字段必填。", "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText": "“时间窗大小”必填。", "xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText": "词字段必填。", - "xpack.triggersActionsUI.sections.addConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "选择连接器", "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "无法创建连接器。", "xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", - "xpack.triggersActionsUI.sections.addFlyout.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", - "xpack.triggersActionsUI.sections.addModalConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", - "xpack.triggersActionsUI.sections.alertAdd.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "定义条件", "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建告警", @@ -20321,7 +20303,6 @@ "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status": "状态", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active": "活动", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive": "确定", - "xpack.triggersActionsUI.sections.alertDetails.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.disableTitle": "禁用", "xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle": "静音", "xpack.triggersActionsUI.sections.alertDetails.dismissButtonTitle": "关闭", @@ -20329,7 +20310,6 @@ "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertInstanceSummaryMessage": "无法加载告警实例摘要:{message}", "xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage": "无法加载告警:{message}", "xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel": "在应用中查看", - "xpack.triggersActionsUI.sections.alertEdit.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.alertEdit.disabledActionsWarningTitle": "此告警具有已禁用的操作", "xpack.triggersActionsUI.sections.alertEdit.flyoutTitle": "编辑告警", @@ -20392,7 +20372,6 @@ "xpack.triggersActionsUI.sections.alertsList.alertStatusOk": "确定", "xpack.triggersActionsUI.sections.alertsList.alertStatusPending": "待处理", "xpack.triggersActionsUI.sections.alertsList.alertStatusUnknown": "未知", - "xpack.triggersActionsUI.sections.alertsList.attentionBannerTitle": "在 {totalStausesError} 个 {totalStausesError, plural, one {{singleTitle}} other {# 个 {multipleTitle}}}中发现错误。", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.buttonTitle": "管理告警", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.deleteAllTitle": "删除", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.disableAllTitle": "禁用", @@ -20443,7 +20422,6 @@ "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.subjectTextFieldLabel": "主题", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel": "用户名", "xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{actionDescription}", - "xpack.triggersActionsUI.sections.editConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "此连接器为只读。", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "编辑连接器", @@ -20453,7 +20431,6 @@ "xpack.triggersActionsUI.sections.editConnectorForm.tabText": "配置", "xpack.triggersActionsUI.sections.editConnectorForm.updateErrorNotificationText": "无法更新连接器。", "xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText": "已更新“{connectorName}”", - "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.flyoutTitle": "{connectorName}", "xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "这是预配置连接器,无法编辑", "xpack.triggersActionsUI.sections.testConnectorForm.awaitingExecutionDescription": "执行该操作时,结果将显示在此处。", diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index c9187096821d8..a4446e0a75120 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -3,9 +3,9 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["alerts", "stackAlerts"], + "optionalPlugins": ["home", "alerts", "stackAlerts"], "requiredPlugins": ["management", "charts", "data", "kibanaReact"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], - "requiredBundles": ["alerts", "esUiShared"] + "requiredBundles": ["home", "alerts", "esUiShared"] } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 958d77a11c883..e22cd268f9bc5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -110,6 +110,7 @@ export interface WebhookConfig { method: string; url: string; headers: Record; + hasAuth: boolean; } export interface WebhookSecrets { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx index 337c1f0f18a93..e4d9d3f009c7e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx @@ -28,7 +28,7 @@ describe('actionTypeRegistry.get() works', () => { }); describe('webhook connector validation', () => { - test('connector validation succeeds when connector config is valid', () => { + test('connector validation succeeds when hasAuth is true and connector config is valid', () => { const actionConnector = { secrets: { user: 'user', @@ -42,6 +42,35 @@ describe('webhook connector validation', () => { method: 'PUT', url: 'http://test.com', headers: { 'content-type': 'text' }, + hasAuth: true, + }, + } as WebhookActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + url: [], + method: [], + user: [], + password: [], + }, + }); + }); + + test('connector validation succeeds when hasAuth is false and connector config is valid', () => { + const actionConnector = { + secrets: { + user: '', + password: '', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + isPreconfigured: false, + config: { + method: 'PUT', + url: 'http://test.com', + headers: { 'content-type': 'text' }, + hasAuth: false, }, } as WebhookActionConnector; @@ -65,6 +94,7 @@ describe('webhook connector validation', () => { name: 'webhook', config: { method: 'PUT', + hasAuth: true, }, } as WebhookActionConnector; @@ -73,7 +103,7 @@ describe('webhook connector validation', () => { url: ['URL is required.'], method: [], user: [], - password: ['Password is required.'], + password: ['Password is required when username is used.'], }, }); }); @@ -90,6 +120,7 @@ describe('webhook connector validation', () => { config: { method: 'PUT', url: 'invalid.url', + hasAuth: true, }, } as WebhookActionConnector; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx index 04077738e6015..db3ba9b78cee6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx @@ -74,26 +74,46 @@ export function getActionType(): ActionTypeModel< ) ); } - if (!action.secrets.user && action.secrets.password) { + if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { errors.user.push( i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHostText', + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText', { defaultMessage: 'Username is required.', } ) ); } - if (!action.secrets.password && action.secrets.user) { + if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { errors.password.push( i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText', + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText', { defaultMessage: 'Password is required.', } ) ); } + if (action.secrets.user && !action.secrets.password) { + errors.password.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText', + { + defaultMessage: 'Password is required when username is used.', + } + ) + ); + } + if (!action.secrets.user && action.secrets.password) { + errors.user.push( + i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText', + { + defaultMessage: 'Username is required when password is used.', + } + ) + ); + } return validationResult; }, validateParams: (actionParams: WebhookActionParams): ValidationResult => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx index 45e4c566f7a27..4c5e78670f0c4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -24,6 +24,7 @@ describe('WebhookActionConnectorFields renders', () => { method: 'PUT', url: 'http:\\test', headers: { 'content-type': 'text' }, + hasAuth: true, }, } as WebhookActionConnector; const wrapper = mountWithIntl( @@ -50,7 +51,9 @@ describe('WebhookActionConnectorFields renders', () => { secrets: {}, actionTypeId: '.webhook', isPreconfigured: false, - config: {}, + config: { + hasAuth: true, + }, } as WebhookActionConnector; const wrapper = mountWithIntl( { method: 'PUT', url: 'http:\\test', headers: { 'content-type': 'text' }, + hasAuth: true, }, } as WebhookActionConnector; const wrapper = mountWithIntl( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index e4f5ef023a529..15d4c6c30450e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useState } from 'react'; +import React, { Fragment, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -34,12 +34,19 @@ const WebhookActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { const { user, password } = action.secrets; - const { method, url, headers } = action.config; + const { method, url, headers, hasAuth } = action.config; const [httpHeaderKey, setHttpHeaderKey] = useState(''); const [httpHeaderValue, setHttpHeaderValue] = useState(''); const [hasHeaders, setHasHeaders] = useState(false); + useEffect(() => { + if (!action.id) { + editActionConfig('hasAuth', true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + if (!method) { editActionConfig('method', 'post'); // set method to POST by default } @@ -268,9 +275,9 @@ const WebhookActionConnectorFields: React.FunctionComponent - +

    -
    -
    - - - - {getEncryptedFieldNotifyLabel(!action.id)} - - - - - - 0 && user !== undefined} + + - 0 && user !== undefined} - name="user" - readOnly={readOnly} - value={user || ''} - data-test-subj="webhookUserInput" - onChange={(e) => { - editActionSecrets('user', e.target.value); - }} - onBlur={() => { - if (!user) { - editActionSecrets('user', ''); - } - }} - /> - - - - 0 && password !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel', - { - defaultMessage: 'Password', + disabled={readOnly} + checked={hasAuth} + onChange={(e) => { + editActionConfig('hasAuth', e.target.checked); + if (!e.target.checked) { + editActionSecrets('user', null); + editActionSecrets('password', null); } - )} - > - 0 && password !== undefined} - value={password || ''} - data-test-subj="webhookPasswordInput" - onChange={(e) => { - editActionSecrets('password', e.target.value); - }} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} - /> - + }} + /> - + {hasAuth ? ( + <> + {getEncryptedFieldNotifyLabel(!action.id)} + + + 0 && user !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel', + { + defaultMessage: 'Username', + } + )} + > + 0 && user !== undefined} + name="user" + readOnly={readOnly} + value={user || ''} + data-test-subj="webhookUserInput" + onChange={(e) => { + editActionSecrets('user', e.target.value); + }} + onBlur={() => { + if (!user) { + editActionSecrets('user', ''); + } + }} + /> + + + + 0 && password !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel', + { + defaultMessage: 'Password', + } + )} + > + 0 && password !== undefined} + value={password || ''} + data-test-subj="webhookPasswordInput" + onChange={(e) => { + editActionSecrets('password', e.target.value); + }} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + /> + + + + + ) : null} - - + + + + + + + ); } return ( - + + + + + ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index f009a04d40978..482b38ffc0d68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -16,11 +16,9 @@ import { EuiTab, EuiTabs, EuiTitle, - EuiBetaBadge, EuiText, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { Section, routeToConnectors, routeToAlerts } from './constants'; import { getAlertingSectionBreadcrumb } from './lib/breadcrumb'; import { getCurrentDocTitle } from './lib/doc_title'; @@ -29,7 +27,6 @@ import { hasShowActionsCapability } from './lib/capabilities'; import { ActionsConnectorsList } from './sections/actions_connectors_list/components/actions_connectors_list'; import { AlertsList } from './sections/alerts_list/components/alerts_list'; -import { PLUGIN } from './constants/plugin'; import { HealthCheck } from './components/health_check'; import { HealthContextProvider } from './context/health_context'; @@ -91,20 +88,6 @@ export const TriggersActionsUIHome: React.FunctionComponent -   -

    diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 060a751677de0..2e222884dab50 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -17,7 +17,6 @@ import { EuiButtonEmpty, EuiButton, EuiFlyoutBody, - EuiBetaBadge, EuiCallOut, EuiSpacer, } from '@elastic/eui'; @@ -31,7 +30,6 @@ import { hasSaveActionsCapability } from '../../lib/capabilities'; import { createActionConnector } from '../../lib/action_connector_api'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; -import { PLUGIN } from '../../constants/plugin'; export interface ConnectorAddFlyoutProps { addFlyoutVisible: boolean; @@ -189,20 +187,6 @@ export const ConnectorAddFlyout = ({ actionTypeName: actionType.name, }} /> -   -

    @@ -216,20 +200,6 @@ export const ConnectorAddFlyout = ({ defaultMessage="Select a connector" id="xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle" /> -   -

    )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 90abb986517d4..d7ca91218d4dd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -5,7 +5,7 @@ */ import React, { useCallback, useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup, EuiBetaBadge } from '@elastic/eui'; +import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup } from '@elastic/eui'; import { EuiModal, EuiButton, @@ -24,7 +24,6 @@ import { connectorReducer } from './connector_reducer'; import { createActionConnector } from '../../lib/action_connector_api'; import { TypeRegistry } from '../../type_registry'; import './connector_add_modal.scss'; -import { PLUGIN } from '../../constants/plugin'; import { hasSaveActionsCapability } from '../../lib/capabilities'; interface ConnectorAddModalProps { @@ -135,20 +134,6 @@ export const ConnectorAddModal = ({ actionTypeName: actionType.name, }} /> -   -

    diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 4d8981f25aedc..e89eb8c95fbab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -31,7 +31,6 @@ import { connectorReducer } from './connector_reducer'; import { updateActionConnector, executeAction } from '../../lib/action_connector_api'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; -import { PLUGIN } from '../../constants/plugin'; import { ActionTypeExecutorResult, isActionTypeExecutorResult, @@ -156,20 +155,6 @@ export const ConnectorEditFlyout = ({ } )} /> -   -

    @@ -187,20 +172,6 @@ export const ConnectorEditFlyout = ({ defaultMessage="Edit connector" id="xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle" /> -   -

    ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 51c3e030f44eb..662db81101eee 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -8,18 +8,8 @@ import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertDetails } from './alert_details'; import { Alert, ActionType, ValidationResult } from '../../../../types'; -import { - EuiTitle, - EuiBadge, - EuiFlexItem, - EuiSwitch, - EuiBetaBadge, - EuiButtonEmpty, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui'; import { ViewInApp } from './view_in_app'; -import { PLUGIN } from '../../../constants/plugin'; import { coreMock } from 'src/core/public/mocks'; import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common'; @@ -104,20 +94,6 @@ describe('alert_details', () => {

    {alert.name} -   -

    ) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 0af01114731a3..1272024557bb6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -22,12 +22,10 @@ import { EuiSwitch, EuiCallOut, EuiSpacer, - EuiBetaBadge, EuiButtonEmpty, EuiButton, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { getAlertingSectionBreadcrumb, getAlertDetailsBreadcrumb } from '../../../lib/breadcrumb'; @@ -39,7 +37,6 @@ import { } from '../../common/components/with_bulk_alert_api_operations'; import { AlertInstancesRouteWithApi } from './alert_instances_route'; import { ViewInApp } from './view_in_app'; -import { PLUGIN } from '../../../constants/plugin'; import { AlertEdit } from '../../alert_form'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { routeToAlertDetails } from '../../../constants'; @@ -130,20 +127,6 @@ export const AlertDetails: React.FunctionComponent = ({

    {alert.name} -   -

    diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 763462ba6ebf4..89deb4b26f012 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -6,14 +6,7 @@ import React, { useCallback, useReducer, useState, useEffect } from 'react'; import { isObject } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiTitle, - EuiFlyoutHeader, - EuiFlyout, - EuiFlyoutBody, - EuiPortal, - EuiBetaBadge, -} from '@elastic/eui'; +import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useAlertsContext } from '../../context/alerts_context'; import { Alert, AlertAction, IErrorObject } from '../../../types'; @@ -21,7 +14,6 @@ import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; -import { PLUGIN } from '../../constants/plugin'; import { ConfirmAlertSave } from './confirm_alert_save'; import { hasShowActionsCapability } from '../../lib/capabilities'; import AlertAddFooter from './alert_add_footer'; @@ -163,20 +155,6 @@ export const AlertAdd = ({ defaultMessage="Create alert" id="xpack.triggersActionsUI.sections.alertAdd.flyoutTitle" /> -   -
    diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 0435a4cc33cb8..5eadc742a9dc8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -16,7 +16,6 @@ import { EuiButton, EuiFlyoutBody, EuiPortal, - EuiBetaBadge, EuiCallOut, EuiSpacer, } from '@elastic/eui'; @@ -27,7 +26,6 @@ import { AlertForm, validateBaseProperties } from './alert_form'; import { alertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; -import { PLUGIN } from '../../constants/plugin'; import { HealthContextProvider } from '../../context/health_context'; interface AlertEditProps { @@ -119,20 +117,6 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { defaultMessage="Edit alert" id="xpack.triggersActionsUI.sections.alertEdit.flyoutTitle" /> -   -
    diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/action_type_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/action_type_filter.tsx index 76c13a46db1a9..5a444cd3e9e60 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/action_type_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/action_type_filter.tsx @@ -40,6 +40,7 @@ export const ActionTypeFilter: React.FunctionComponent = numActiveFilters={selectedValues.length} numFilters={selectedValues.length} onClick={() => setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="actionTypeFilterButton" > = } }} checked={selectedValues.includes(item.id) ? 'on' : undefined} + data-test-subj={`actionType${item.id}FilterOption`} > {item.name} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx index 87e7a82cd8f23..1bae0b885dbfa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx @@ -54,6 +54,7 @@ export const AlertStatusFilter: React.FunctionComponent numActiveFilters={selectedValues.length} numFilters={selectedValues.length} onClick={() => setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="alertStatusFilterButton" > } }} checked={selectedValues.includes(item) ? 'on' : undefined} + data-test-subj={`alertStatus${item}FilerOption`} > {alertsStatusesTranslationsMapping[item]} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 1ef695f3ed112..9eb1149cf3905 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -437,15 +437,14 @@ export const AlertsList: React.FunctionComponent = () => { title={ } iconType="alert" + data-test-subj="alertsErrorBanner" > { - + { - + { - + - + - + { - + = ({ numActiveFilters={selectedValues.length} numFilters={selectedValues.length} onClick={() => setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="alertTypeFilterButton" > = ({ } }} checked={selectedValues.includes(item.value) ? 'on' : undefined} + data-test-subj={`alertType${item.value}FilterOption`} > {item.name} diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 874a380f56b5f..393ac5bc1b74d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -20,6 +20,10 @@ import { ManagementAppMountParams, ManagementSetup, } from '../../../../src/plugins/management/public'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { PluginStartContract as AlertingStart } from '../../alerts/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; @@ -40,6 +44,7 @@ export interface TriggersAndActionsUIPublicPluginStart { interface PluginsSetup { management: ManagementSetup; + home?: HomePublicPluginSetup; } interface PluginsStart { @@ -73,11 +78,31 @@ export class Plugin const actionTypeRegistry = this.actionTypeRegistry; const alertTypeRegistry = this.alertTypeRegistry; + const featureTitle = i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { + defaultMessage: 'Alerts and Actions', + }); + const featureDescription = i18n.translate( + 'xpack.triggersActionsUI.managementSection.displayDescription', + { + defaultMessage: 'Detect conditions using alerts, and take actions using connectors.', + } + ); + + if (plugins.home) { + plugins.home.featureCatalogue.register({ + id: 'triggersActions', + title: featureTitle, + description: featureDescription, + icon: 'watchesApp', + path: '/app/management/insightsAndAlerting/triggersActions', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); + } + plugins.management.sections.section.insightsAndAlerting.registerApp({ id: 'triggersActions', - title: i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { - defaultMessage: 'Alerts and Actions', - }), + title: featureTitle, order: 0, async mount(params: ManagementAppMountParams) { const [coreStart, pluginsStart] = (await core.getStartServices()) as [ diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 148facdee248d..f24a9a2e3b658 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup, DocLinksStart, ToastsSetup } from 'kibana/public'; +import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { HttpSetup, DocLinksStart, ToastsSetup } from 'kibana/public'; import { ComponentType } from 'react'; import { ActionGroup } from '../../alerts/common'; import { ActionType } from '../../actions/common'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts index 78eec05eb2d0b..06f63949f6d63 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts @@ -5,6 +5,7 @@ */ import uuid from 'uuid'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, UiActionsEnhancedDynamicActionManagerState as DynamicActionManagerState, diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts new file mode 100644 index 0000000000000..feda1c93e2511 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; +import { UiActionsPresentableGrouping as PresentableGrouping } from '../../../../../src/plugins/ui_actions/public'; + +export const dynamicActionGrouping: PresentableGrouping<{ + embeddable?: IEmbeddable; +}> = [ + { + id: 'dynamicActions', + getDisplayName: () => + i18n.translate('xpack.uiActionsEnhanced.CustomActions', { + defaultMessage: 'Custom actions', + }), + getIconType: () => 'symlink', + order: 26, + }, +]; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index cdd357f3560b8..cbc381c911c3d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -13,6 +13,7 @@ import { UiActionsServiceEnhancements } from '../services'; import { ActionFactoryDefinition } from './action_factory_definition'; import { SerializedAction, SerializedEvent } from './types'; import { licensingMock } from '../../../licensing/public/mocks'; +import { dynamicActionGrouping } from './dynamic_action_grouping'; const actionFactoryDefinition1: ActionFactoryDefinition = { id: 'ACTION_FACTORY_1', @@ -294,6 +295,27 @@ describe('DynamicActionManager', () => { expect(manager.state.get().events.length).toBe(1); }); + test('adds revived actiosn to "dynamic action" grouping', async () => { + const { manager, uiActions, actions } = setup([]); + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(manager.state.get().events.length).toBe(0); + + await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']); + + const createdAction = actions.values().next().value; + + expect(createdAction.grouping).toBe(dynamicActionGrouping); + }); + test('optimistically adds event to UI state', async () => { const { manager, uiActions } = setup([]); const action: SerializedAction = { diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index b414296690c9e..f096b17f8a78d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -18,6 +18,7 @@ import { } from '../../../../../src/plugins/kibana_utils/common'; import { StartContract } from '../plugin'; import { SerializedAction, SerializedEvent } from './types'; +import { dynamicActionGrouping } from './dynamic_action_grouping'; const compareEvents = ( a: ReadonlyArray<{ eventId: string }>, @@ -93,6 +94,7 @@ export class DynamicActionManager { uiActions.registerAction({ ...actionDefinition, id: actionId, + grouping: dynamicActionGrouping, isCompatible: async (context) => { if (!(await isCompatible(context))) return false; if (!actionDefinition.isCompatible) return true; diff --git a/x-pack/plugins/upgrade_assistant/README.md b/x-pack/plugins/upgrade_assistant/README.md new file mode 100644 index 0000000000000..a6cb3b431c82b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/README.md @@ -0,0 +1,67 @@ +# Upgrade Assistant + +## About + +Upgrade Assistant helps users prepare their Stack for being upgraded to the next major. Its primary +purposes are to: + +* **Surface deprecations.** Deprecations are features that are currently being used that will be +removed in the next major. Surfacing tells the user that there's a problem preventing them +from upgrading. +* **Migrate from deprecation features to supported features.** This addresses the problem, clearing +the path for the upgrade. Generally speaking, once all deprecations are addressed, the user can +safely upgrade. + +### Deprecations + +There are two sources of deprecation information: + +* [**Deprecation Info API.**](https://www.elastic.co/guide/en/elasticsearch/reference/master/migration-api-deprecation.html) +This is information about cluster, node, and index level settings that use deprecated features that +will be removed or changed in the next major version. Currently, only cluster and index deprecations +will be surfaced in the Upgrade Assistant. ES server engineers are responsible for adding +deprecations to the Deprecation Info API. +* [**Deprecation logs.**](https://www.elastic.co/guide/en/elasticsearch/reference/current/logging.html#deprecation-logging) +These surface runtime deprecations, e.g. a Painless script that uses a deprecated accessor or a +request to a deprecated API. These are also generally surfaced as deprecation headers within the +response. Even if the cluster state is good, app maintainers need to watch the logs in case +deprecations are discovered as data is migrated. + +### Fixing problems + +Problems can be fixed at various points in the upgrade process. The Upgrade Assistant supports +various upgrade paths and surfaces various types of upgrade-related issues. + +* **Fixing deprecated cluster settings pre-upgrade.** This generally requires fixing some settings +in `elasticsearch.yml`. +* **Migrating indices data pre-upgrade.** This can involve deleting indices so that ES can rebuild +them in the new version, reindexing them so that they're built using a new Lucene version, or +applying a migration script that reindexes them with new settings/mappings/etc. +* **Migrating indices data post-upgrade.** As was the case with APM in the 6.8->7.x upgrade, +sometimes the new data format isn't forwards-compatible. In these cases, the user will perform the +upgrade first and then use the Upgrade Assistant to reindex their data to be compatible with the new +version. + +Deprecations can be handled in a number of ways: + +* **Reindexing.** When a user's index contains deprecations (e.g. mappings) a reindex solves them. +Upgrade Assistant contains migration scripts that are executed as part of the reindex process. +The user will see a "Reindex" button they can click which will apply this script and perform the +reindex. + * Reindexing is an atomic process in Upgrade Assistant, so that ingestion is never disrupted. + It works like this: + * Create a new index with a "reindexed-" prefix ([#30114](https://github.com/elastic/kibana/pull/30114)). + * Create an index alias pointing from the original index name to the prefixed index name. + * Reindex from the original index into the prefixed index. + * Delete the old index and rename the prefixed index. + * Some apps might require custom scripts, as was the case with APM ([#29845](https://github.com/elastic/kibana/pull/29845)). + In that case the migration performed a reindex with a Painless script (covered by automated tests) + that made the required changes to the data. +* **Update index settings.** Some index settings will need to be updated, which doesn't require a +reindex. An example of this is the "Fix" button that was added for metricbeat and filebeat indices +([#32829](https://github.com/elastic/kibana/pull/32829), [#33439](https://github.com/elastic/kibana/pull/33439)). +* **Following the docs.** The Deprecation Info API provides links to the deprecation docs. Users +will follow these docs to address the problem and make these warnings or errors disappear in the +Upgrade Assistant. +* **Stopping/restarting tasks and jobs.** Users had to stop watches and ML jobs and restart them as +soon as reindexing was complete ([#29663](https://github.com/elastic/kibana/pull/29663)). \ No newline at end of file diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx index 5ab815a3c0b5d..9fec9439b3ad5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx @@ -253,6 +253,9 @@ describe('ExecutedJourney component', () => { } } /> + `); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx index 2ffb3f0feb4dd..9a3e045017f9a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx @@ -79,6 +79,7 @@ export const ExecutedJourney: FC = ({ journey }) => ( {journey.steps.filter(isStepEnd).map((step, index) => ( ))} +
    ); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx index 3c26ba12eea65..5966851973af2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx @@ -41,7 +41,7 @@ export const ExecutedStep: FC = ({ step, index }) => (
    - +
    @@ -87,6 +87,5 @@ export const ExecutedStep: FC = ({ step, index }) => (
    - ); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx index 2e8ad4bd0c9a8..b81cf6bc1ec1d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx @@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useContext, useEffect, useRef, useState, FC } from 'react'; import { useIntersection } from 'react-use'; -import { UptimeThemeContext } from '../../../contexts'; +import { UptimeSettingsContext, UptimeThemeContext } from '../../../contexts'; interface StepScreenshotDisplayProps { screenshotExists?: boolean; @@ -41,6 +41,8 @@ export const StepScreenshotDisplay: FC = ({ colors: { lightestShade: pageBackground }, } = useContext(UptimeThemeContext); + const { basePath } = useContext(UptimeSettingsContext); + const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false); const [isOverlayOpen, setIsOverlayOpen] = useState(false); @@ -59,7 +61,7 @@ export const StepScreenshotDisplay: FC = ({ }, [hasIntersected, isIntersecting, setHasIntersected]); let content: JSX.Element | null = null; - const imgSrc = `/api/uptime/journey/screenshot/${checkGroup}/${stepIndex}`; + const imgSrc = basePath + `/api/uptime/journey/screenshot/${checkGroup}/${stepIndex}`; if (hasIntersected && screenshotExists) { content = ( <> diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index e4392480f5b72..d0bb67795d46c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -35,6 +35,7 @@ export const getJourneySteps: UMElasticsearchQueryFn = { headers: null, method: 'post', + hasAuth: true, }; function parsePort(url: Record): Record { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts index 1dd88832e38b7..d46d60905da1c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts @@ -30,25 +30,47 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body.config).key('incidentConfiguration'); expect(response.body.config).not.key('casesConfiguration'); - expect(response.body.config.incidentConfiguration).to.eql({ - mapping: [ - { - actionType: 'overwrite', - source: 'title', - target: 'summary', - }, - { - actionType: 'overwrite', - source: 'description', - target: 'description', - }, - { - actionType: 'append', - source: 'comments', - target: 'comments', - }, - ], + expect(response.body.config).to.eql({ + apiUrl: + 'http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/jira', + incidentConfiguration: { + mapping: [ + { + actionType: 'overwrite', + source: 'title', + target: 'summary', + }, + { + actionType: 'overwrite', + source: 'description', + target: 'description', + }, + { + actionType: 'append', + source: 'comments', + target: 'comments', + }, + ], + }, + projectKey: 'CK', }); }); + + it('7.11.0 migrates webhook connector configurations to have `hasAuth` property', async () => { + const responseWithAuth = await supertest.get( + `${getUrlPrefix(``)}/api/actions/action/949f909b-20a0-46e3-aadb-6a4d117bb592` + ); + + expect(responseWithAuth.status).to.eql(200); + expect(responseWithAuth.body.config).key('hasAuth'); + expect(responseWithAuth.body.config.hasAuth).to.eql(true); + + const responseNoAuth = await supertest.get( + `${getUrlPrefix(``)}/api/actions/action/7434121e-045a-47d6-a0a6-0b6da752397a` + ); + expect(responseNoAuth.status).to.eql(200); + expect(responseNoAuth.body.config).key('hasAuth'); + expect(responseNoAuth.body.config.hasAuth).to.eql(false); + }); }); } diff --git a/x-pack/test/api_integration/apis/maps/index.js b/x-pack/test/api_integration/apis/maps/index.js index 9f1fe96c5637b..36a8fa2ec045e 100644 --- a/x-pack/test/api_integration/apis/maps/index.js +++ b/x-pack/test/api_integration/apis/maps/index.js @@ -18,6 +18,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./migrations')); loadTestFile(require.resolve('./get_tile')); loadTestFile(require.resolve('./get_grid_tile')); + loadTestFile(require.resolve('./proxy_api')); }); }); } diff --git a/x-pack/test/api_integration/apis/maps/proxy_api.js b/x-pack/test/api_integration/apis/maps/proxy_api.js new file mode 100644 index 0000000000000..5dc6b372ced70 --- /dev/null +++ b/x-pack/test/api_integration/apis/maps/proxy_api.js @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + + describe('EMS proxy', () => { + it('should correctly rewrite url and format', async () => { + const resp = await supertest + .get(`/api/maps/ems/files/v7.10/manifest`) + .set('kbn-xsrf', 'kibana') + .expect(200); + + expect(resp.body.layers.length).to.be.greaterThan(0); + + //Check world-layer + const worldLayer = resp.body.layers.find((layer) => layer.layer_id === 'world_countries'); + expect(worldLayer.formats.length).to.be.greaterThan(0); + expect(worldLayer.formats[0].type).to.be('topojson'); + expect(worldLayer.formats[0].url).to.be('file?id=world_countries'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_item.ts b/x-pack/test/api_integration/apis/metrics_ui/log_item.ts index 6e0148f13277c..3bb7a9a76690d 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/log_item.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/log_item.ts @@ -44,107 +44,107 @@ export default function ({ getService }: FtrProviderContext) { expect(logItem.fields).to.eql([ { field: '@timestamp', - value: '2018-10-17T19:42:22.000Z', + value: ['2018-10-17T19:42:22.000Z'], }, { field: '_id', - value: 'yT2Mg2YBh-opCxJv8Vqj', + value: ['yT2Mg2YBh-opCxJv8Vqj'], }, { field: '_index', - value: 'filebeat-7.0.0-alpha1-2018.10.17', + value: ['filebeat-7.0.0-alpha1-2018.10.17'], }, { field: 'apache2.access.body_sent.bytes', - value: '1336', + value: ['1336'], }, { field: 'apache2.access.http_version', - value: '1.1', + value: ['1.1'], }, { field: 'apache2.access.method', - value: 'GET', + value: ['GET'], }, { field: 'apache2.access.referrer', - value: '-', + value: ['-'], }, { field: 'apache2.access.remote_ip', - value: '10.128.0.11', + value: ['10.128.0.11'], }, { field: 'apache2.access.response_code', - value: '200', + value: ['200'], }, { field: 'apache2.access.url', - value: '/a-fresh-start-will-put-you-on-your-way', + value: ['/a-fresh-start-will-put-you-on-your-way'], }, { field: 'apache2.access.user_agent.device', - value: 'Other', + value: ['Other'], }, { field: 'apache2.access.user_agent.name', - value: 'Other', + value: ['Other'], }, { field: 'apache2.access.user_agent.os', - value: 'Other', + value: ['Other'], }, { field: 'apache2.access.user_agent.os_name', - value: 'Other', + value: ['Other'], }, { field: 'apache2.access.user_name', - value: '-', + value: ['-'], }, { field: 'beat.hostname', - value: 'demo-stack-apache-01', + value: ['demo-stack-apache-01'], }, { field: 'beat.name', - value: 'demo-stack-apache-01', + value: ['demo-stack-apache-01'], }, { field: 'beat.version', - value: '7.0.0-alpha1', + value: ['7.0.0-alpha1'], }, { field: 'fileset.module', - value: 'apache2', + value: ['apache2'], }, { field: 'fileset.name', - value: 'access', + value: ['access'], }, { field: 'host.name', - value: 'demo-stack-apache-01', + value: ['demo-stack-apache-01'], }, { field: 'input.type', - value: 'log', + value: ['log'], }, { field: 'offset', - value: '5497614', + value: ['5497614'], }, { field: 'prospector.type', - value: 'log', + value: ['log'], }, { field: 'read_timestamp', - value: '2018-10-17T19:42:23.160Z', + value: ['2018-10-17T19:42:23.160Z'], }, { field: 'source', - value: '/var/log/apache2/access.log', + value: ['/var/log/apache2/access.log'], }, ]); }); diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts index 3e06373042d59..69571099a2642 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts @@ -24,6 +24,30 @@ export default function ({ getService }: FtrProviderContext) { before('load heartbeat data', () => getService('esArchiver').load('uptime/blank')); after('unload heartbeat index', () => getService('esArchiver').unload('uptime/blank')); + // In this case we don't actually have any monitors to display + // but the query should still return successfully. This has + // caused bugs in the past because a bucket of monitor data + // was available and the query code assumed at least one + // event would be a summary for each monitor. + // See https://github.com/elastic/kibana/issues/81950 + describe('checks with no summaries', async () => { + const testMonitorId = 'scope-test-id'; + before(async () => { + const es = getService('legacyEs'); + dateRangeStart = new Date().toISOString(); + await makeChecksWithStatus(es, testMonitorId, 1, numIps, 1, {}, 'up', (d) => { + delete d.summary; + return d; + }); + }); + + it('should return no monitors and have no errors', async () => { + const url = getBaseUrl(dateRangeStart, new Date().toISOString()); + const apiResponse = await supertest.get(url); + expect(apiResponse.status).to.equal(200); + }); + }); + describe('query document scoping with mismatched check statuses', async () => { let checks: any[] = []; let nonSummaryIp: string | null = null; diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 97fd968ce7992..fc021ffa0ca5b 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -25,6 +25,7 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi ...xPackFunctionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--map.proxyElasticMapsServiceInMaps=true', '--xpack.security.session.idleTimeout=3600000', // 1 hour '--telemetry.optIn=true', '--xpack.fleet.enabled=true', diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts index efbdb75c47cc1..5dbe266deeb81 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts @@ -21,14 +21,12 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) expect(response.status).to.be(200); expect(response.body).to.eql({ - cls: '0', - fid: '0.00', - lcp: '0.00', - tbt: '0.00', - fcp: 0, - lcpRanks: [0, 0, 100], - fidRanks: [0, 0, 100], - clsRanks: [0, 0, 100], + coreVitalPages: 0, + cls: null, + tbt: 0, + lcpRanks: [100, 0, 0], + fidRanks: [100, 0, 0], + clsRanks: [100, 0, 0], }); }); }); @@ -52,20 +50,21 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) expectSnapshot(response.body).toMatchInline(` Object { - "cls": "0.00", + "cls": "0.000", "clsRanks": Array [ 100, 0, 0, ], - "fcp": 1072, + "coreVitalPages": 6, + "fcp": 817.5, "fid": 1352.13, "fidRanks": Array [ 0, 0, 100, ], - "lcp": 1270.5, + "lcp": 1019, "lcpRanks": Array [ 100, 0, diff --git a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts index 004e51e459caa..4d37ee1589169 100644 --- a/x-pack/test/functional/apps/dashboard/async_search/async_search.ts +++ b/x-pack/test/functional/apps/dashboard/async_search/async_search.ts @@ -8,13 +8,26 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const es = getService('es'); const testSubjects = getService('testSubjects'); + const log = getService('log'); const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'visChart']); + const dashboardPanelActions = getService('dashboardPanelActions'); + const inspector = getService('inspector'); + const queryBar = getService('queryBar'); describe('dashboard with async search', () => { + before(async function () { + const { body } = await es.info(); + if (!body.version.number.includes('SNAPSHOT')) { + log.debug('Skipping because this build does not have the required shard_delay agg'); + this.skip(); + } + }); + it('not delayed should load', async () => { await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('Not Delayed'); + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('embeddableErrorLabel'); const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); @@ -23,7 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('delayed should load', async () => { await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('Delayed 5s'); + await PageObjects.dashboard.loadSavedDashboard('Delayed 5s'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('embeddableErrorLabel'); const data = await PageObjects.visChart.getBarChartData(''); @@ -32,10 +45,47 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('timed out should show error', async () => { await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.gotoDashboardEditMode('Delayed 15s'); + await PageObjects.dashboard.loadSavedDashboard('Delayed 15s'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('embeddableErrorLabel'); await testSubjects.existOrFail('searchTimeoutError'); }); + + it('multiple searches are grouped and only single error popup is shown', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Multiple delayed'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('embeddableErrorLabel'); + // there should be two failed panels + expect((await testSubjects.findAll('embeddableErrorLabel')).length).to.be(2); + // but only single error toast because searches are grouped + expect((await testSubjects.findAll('searchTimeoutError')).length).to.be(1); + + // check that session ids are the same + const getSearchSessionIdByPanel = async (panelTitle: string) => { + await dashboardPanelActions.openInspectorByTitle(panelTitle); + await inspector.openInspectorRequestsView(); + const searchSessionId = await ( + await testSubjects.find('inspectorRequestSearchSessionId') + ).getAttribute('data-search-session-id'); + await inspector.close(); + return searchSessionId; + }; + + const panel1SessionId1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + const panel2SessionId1 = await getSearchSessionIdByPanel( + 'Sum of Bytes by Extension (Delayed 5s)' + ); + expect(panel1SessionId1).to.be(panel2SessionId1); + + await queryBar.clickQuerySubmitButton(); + + const panel1SessionId2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); + const panel2SessionId2 = await getSearchSessionIdByPanel( + 'Sum of Bytes by Extension (Delayed 5s)' + ); + expect(panel1SessionId2).to.be(panel2SessionId2); + expect(panel1SessionId1).not.to.be(panel1SessionId2); + }); }); } diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index de6353d6b0456..78edbafa4ae47 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -58,7 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs']); + expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); }); describe('logs landing page without data', () => { @@ -121,7 +121,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs']); + expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); }); describe('logs landing page without data', () => { diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/persistent_context.ts index 15515df92a89f..8d536aac3f795 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/persistent_context.ts @@ -66,5 +66,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(timeRange.end).to.equal('Sep 19, 2025 @ 06:31:44.000'); await filterBar.hasFilter('ip', '97.220.3.248', false, true); }); + + it('keeps time range and pinned filters after refreshing directly after saving', async () => { + // restore defaults so visualization becomes saveable + await security.testUser.restoreDefaults(); + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + await PageObjects.lens.save('persistentcontext'); + await browser.refresh(); + await PageObjects.header.waitUntilLoadingHasFinished(); + const timeRange = await PageObjects.timePicker.getTimeConfig(); + expect(timeRange.start).to.equal('Sep 7, 2015 @ 06:31:44.000'); + expect(timeRange.end).to.equal('Sep 19, 2025 @ 06:31:44.000'); + await filterBar.hasFilter('ip', '97.220.3.248', false, true); + }); }); } diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index 3c935b385cb39..0c8a208e92ece 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -15,6 +15,7 @@ export default function ({ getPageObjects, getService }) { const inspector = getService('inspector'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); + const retry = getService('retry'); describe('embed in dashboard', () => { before(async () => { @@ -50,9 +51,11 @@ export default function ({ getPageObjects, getService }) { it('should populate inspector with requests for map embeddable', async () => { await dashboardPanelActions.openInspectorByTitle('join example'); - const joinExampleRequestNames = await inspector.getRequestNames(); + await retry.try(async () => { + const joinExampleRequestNames = await inspector.getRequestNames(); + expect(joinExampleRequestNames).to.equal('geo_shapes*,meta_for_geo_shapes*.shape_name'); + }); await inspector.close(); - expect(joinExampleRequestNames).to.equal('geo_shapes*,meta_for_geo_shapes*.shape_name'); await dashboardPanelActions.openInspectorByTitle('geo grid vector grid example'); const gridExampleRequestNames = await inspector.getRequestNames(); diff --git a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts index 2ee6b903cc3a9..8f29ae6a27c3a 100644 --- a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts +++ b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts @@ -115,5 +115,33 @@ export default function spaceSelectorFunctonalTests({ await PageObjects.copySavedObjectsToSpace.finishCopy(); }); + + it('allows a dashboard to be copied to the marketing space, with circular references', async () => { + const destinationSpaceId = 'marketing'; + + await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('Dashboard Foo'); + + await PageObjects.copySavedObjectsToSpace.setupForm({ + overwrite: true, + destinationSpaceId, + }); + + await PageObjects.copySavedObjectsToSpace.startCopy(); + + // Wait for successful copy + await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`); + await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`); + + const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); + + expect(summaryCounts).to.eql({ + success: 2, + pending: 0, + skipped: 0, + errors: 0, + }); + + await PageObjects.copySavedObjectsToSpace.finishCopy(); + }); }); } diff --git a/x-pack/test/functional/es_archives/actions/data.json b/x-pack/test/functional/es_archives/actions/data.json index aeeca87deb9ff..18d67da1752bc 100644 --- a/x-pack/test/functional/es_archives/actions/data.json +++ b/x-pack/test/functional/es_archives/actions/data.json @@ -56,3 +56,57 @@ "type": "_doc" } } + +{ + "type": "doc", + "value": { + "id": "action:949f909b-20a0-46e3-aadb-6a4d117bb592", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId": ".webhook", + "config": { + "headers": null, + "method": "post", + "url": "http://localhost" + }, + "name": "A webhook with auth", + "secrets": "LUqlrITACjqPmcWGlbl+H4RsGGOlw8LM0Urq8r7y6jNT7Igv3J7FjKJ2NXfNTaghVBO7e9x3wZOtiycwyoAdviTyYm1pspni24vH+OT70xaSuXcDoxfGwiLEcaG04INDnUJX4dtmRerxqR9ChktC70LNtOU3sqjYI2tWt2vOqGeq" + }, + "migrationVersion": { + "action": "7.10.0" + }, + "references": [ + ], + "type": "action", + "updated_at": "2020-10-26T21:29:47.380Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "action:7434121e-045a-47d6-a0a6-0b6da752397a", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId": ".webhook", + "config": { + "headers": null, + "method": "post", + "url": "http://localhost" + }, + "name": "A webhook with no auth", + "secrets": "tOwFq20hbUrcp3FX7stKB5aJaQQdLNQwomSNym8BgnFaBBafPOASv5T0tGdGsTr/CA7VK+N/wYBHQPzt0apF8Z/UYl63ZXqck5tSoFDnQW77zv1VVQ5wEwN1qkAQQcfrXTXU2wYVAYZNSuHkbeRjcasfG0ty1K+J7A==" + }, + "migrationVersion": { + "action": "7.10.0" + }, + "references": [ + ], + "type": "action", + "updated_at": "2020-10-26T21:30:35.146Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/data.json b/x-pack/test/functional/es_archives/dashboard/async_search/data.json index 2990097e88d00..486c73f711a6b 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/data.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/data.json @@ -194,4 +194,52 @@ } } +{ + "type": "doc", + "value": { + "id": "dashboard:a41c6790-075d-11eb-be70-0bd5e8b57d03", + "index": ".kibana", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "optionsJSON": "{\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"ec585931-ce8e-43fd-aa94-a1a9612d24ba\"},\"panelIndex\":\"ec585931-ce8e-43fd-aa94-a1a9612d24ba\",\"embeddableConfig\":{},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"c7b18010-462b-4e55-a974-fdec2ae64b06\"},\"panelIndex\":\"c7b18010-462b-4e55-a974-fdec2ae64b06\",\"embeddableConfig\":{},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"e67704f7-20b7-4ade-8dee-972a9d187107\"},\"panelIndex\":\"e67704f7-20b7-4ade-8dee-972a9d187107\",\"embeddableConfig\":{},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"f0b03592-10f1-41cd-9929-0cb4163bcd16\"},\"panelIndex\":\"f0b03592-10f1-41cd-9929-0cb4163bcd16\",\"embeddableConfig\":{},\"panelRefName\":\"panel_3\"}]", + "refreshInterval": { "pause": true, "value": 0 }, + "timeFrom": "2015-09-19T17:34:10.297Z", + "timeRestore": true, + "timeTo": "2015-09-23T00:09:17.180Z", + "title": "Multiple delayed", + "version": 1 + }, + "references": [ + { + "id": "14501a50-01e3-11eb-9b63-176d7b28a352", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "50a67010-075d-11eb-be70-0bd5e8b57d02", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "6c9f3830-01e3-11eb-9b63-176d7b28a352", + "name": "panel_2", + "type": "visualization" + }, + { + "id": "50a67010-075d-11eb-be70-0bd5e8b57d02", + "name": "panel_3", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-03-19T11:59:53.701Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json b/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json new file mode 100644 index 0000000000000..4e871f6308b77 --- /dev/null +++ b/x-pack/test/functional/es_archives/event_log_multiple_indicies/data.json @@ -0,0 +1,274 @@ +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "buildNum": 9007199254740991 + }, + "migrationVersion": { + "config": "7.9.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2020-10-28T15:19:15.795Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [ + ], + "space": { + "_reserved": true, + "color": "#00bfb3", + "description": "This is your default space!", + "disabledFeatures": [ + ], + "name": "Default" + }, + "type": "space", + "updated_at": "2020-10-28T15:19:15.857Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:namespace-a", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [ + ], + "space": { + "disabledFeatures": [ + ], + "name": "Space A" + }, + "type": "space", + "updated_at": "2020-10-28T15:19:52.887Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "event_log_test:421f2511-5cd1-44fd-95df-e0df83e354d5", + "index": ".kibana_1", + "source": { + "event_log_test": { + }, + "references": [ + ], + "type": "event_log_test", + "updated_at": "2020-10-28T15:19:53.861Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "XKbLb3UBt6Z_MVvSSPbe", + "index": ".kibana-event-log-7.9.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:54.841Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:54.841Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:54.841Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:53.825Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "XabLb3UBt6Z_MVvSSfYD", + "index": ".kibana-event-log-7.9.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:54.879Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:54.879Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:54.879Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:54.849Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "XqbLb3UBt6Z_MVvSSfYe", + "index": ".kibana-event-log-7.9.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:54.905Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:54.905Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:54.905Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:54.881Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "X6bLb3UBt6Z_MVvSTfYk", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.933Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.933Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.933Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:55.913Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "YKbLb3UBt6Z_MVvSTfY8", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.957Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.957Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.957Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:55.938Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "YabLb3UBt6Z_MVvSTfZc", + "index": ".kibana-event-log-8.0.0-000001", + "source": { + "@timestamp": "2020-10-28T15:19:55.991Z", + "ecs": { + "version": "1.5.0" + }, + "event": { + "action": "test", + "duration": 0, + "end": "2020-10-28T15:19:55.991Z", + "provider": "event_log_fixture", + "start": "2020-10-28T15:19:55.991Z" + }, + "kibana": { + "saved_objects": [ + { + "id": "421f2511-5cd1-44fd-95df-e0df83e354d5", + "rel": "primary", + "type": "event_log_test" + } + ], + "server_uuid": "5b2de169-2785-441b-ae8c-186a1936b17d" + }, + "message": "test 2020-10-28T15:19:55.962Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json b/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json new file mode 100644 index 0000000000000..b418ccc1343a2 --- /dev/null +++ b/x-pack/test/functional/es_archives/event_log_multiple_indicies/mappings.json @@ -0,0 +1,576 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "eaf6f5841dbf4cb5e3045860f75f53ca", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "2b83397e3eaaaa8ef15e38813f3721c3", + "event_log_test": "bef808d4a9c27f204ffbda3359233931", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "f74dfe498e1849267cda41580b2be110", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "event_log_test": { + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "monitoring-telemetry": { + "properties": { + "reportedClusterUuids": { + "type": "keyword" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana-event-log-7.9.0": { + "is_write_index": true + } + }, + "index": ".kibana-event-log-7.9.0-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "message": { + "norms": false, + "type": "text" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + } + } + }, + "kibana": { + "properties": { + "alerting": { + "properties": { + "instance_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "saved_objects": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "rel": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "server_uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "tags": { + "ignore_above": 1024, + "meta": { + "isArray": "true" + }, + "type": "keyword" + }, + "user": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "lifecycle": { + "name": "kibana-event-log-policy", + "rollover_alias": ".kibana-event-log-7.9.0" + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana-event-log-8.0.0": { + "is_write_index": true + } + }, + "index": ".kibana-event-log-8.0.0-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "message": { + "norms": false, + "type": "text" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + } + } + }, + "kibana": { + "properties": { + "alerting": { + "properties": { + "instance_id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "saved_objects": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "rel": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "server_uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "tags": { + "ignore_above": 1024, + "meta": { + "isArray": "true" + }, + "type": "keyword" + }, + "user": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "lifecycle": { + "name": "kibana-event-log-policy", + "rollover_alias": ".kibana-event-log-8.0.0" + }, + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json index 944b91e8be114..3434e1f80a7ce 100644 --- a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json +++ b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json @@ -109,3 +109,63 @@ } } } + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:dashboard-foo", + "source": { + "references": [{ + "id":"dashboard-bar", + "name":"dashboard-circular-ref", + "type":"dashboard" + }], + "dashboard": { + "title": "Dashboard Foo", + "hits": 0, + "description": "", + "panelsJSON": "[{}]", + "optionsJSON": "{}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:dashboard-bar", + "source": { + "references": [{ + "id":"dashboard-foo", + "name":"dashboard-circular-ref", + "type":"dashboard" + }], + "dashboard": { + "title": "Dashboard Bar", + "hits": 0, + "description": "", + "panelsJSON": "[{}]", + "optionsJSON": "{}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 408a50be8882e..c4f1bd7dc2a6b 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -20,6 +20,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte const comboBox = getService('comboBox'); const renderable = getService('renderable'); const browser = getService('browser'); + const MenuToggle = getService('MenuToggle'); + + const setViewPopoverToggle = new MenuToggle({ + name: 'SetView Popover', + menuTestSubject: 'mapSetViewForm', + toggleButtonTestSubject: 'toggleSetViewVisibilityButton', + }); function escapeLayerName(layerName: string) { return layerName.split(' ').join('_'); @@ -234,41 +241,11 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte return buttons.length; } - async isSetViewPopoverOpen() { - return await testSubjects.exists('mapSetViewForm', { timeout: 500 }); - } - - async openSetViewPopover() { - const isOpen = await this.isSetViewPopoverOpen(); - if (!isOpen) { - await retry.try(async () => { - await testSubjects.click('toggleSetViewVisibilityButton'); - const isOpenAfterClick = await this.isSetViewPopoverOpen(); - if (!isOpenAfterClick) { - throw new Error('set view popover not opened'); - } - }); - } - } - - async closeSetViewPopover() { - const isOpen = await this.isSetViewPopoverOpen(); - if (isOpen) { - await retry.try(async () => { - await testSubjects.click('toggleSetViewVisibilityButton'); - const isOpenAfterClick = await this.isSetViewPopoverOpen(); - if (isOpenAfterClick) { - throw new Error('set view popover not closed'); - } - }); - } - } - async setView(lat: number, lon: number, zoom: number) { log.debug( `Set view lat: ${lat.toString()}, lon: ${lon.toString()}, zoom: ${zoom.toString()}` ); - await this.openSetViewPopover(); + await setViewPopoverToggle.open(); await testSubjects.setValue('latitudeInput', lat.toString()); await testSubjects.setValue('longitudeInput', lon.toString()); await testSubjects.setValue('zoomInput', zoom.toString()); @@ -278,11 +255,20 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte async getView() { log.debug('Get view'); - await this.openSetViewPopover(); - const lat = await testSubjects.getAttribute('latitudeInput', 'value'); - const lon = await testSubjects.getAttribute('longitudeInput', 'value'); - const zoom = await testSubjects.getAttribute('zoomInput', 'value'); - await this.closeSetViewPopover(); + await setViewPopoverToggle.open(); + // this method is regularly called within a retry, so we need to reduce the timeouts + // of the retries done within the getAttribute method in order to ensure that they fail + // early enough to retry getView() + const getAttributeOptions = { + tryTimeout: 5000, + findTimeout: 1000, + }; + + const lat = await testSubjects.getAttribute('latitudeInput', 'value', getAttributeOptions); + const lon = await testSubjects.getAttribute('longitudeInput', 'value', getAttributeOptions); + const zoom = await testSubjects.getAttribute('zoomInput', 'value', getAttributeOptions); + + await setViewPopoverToggle.close(); return { lat: parseFloat(lat), lon: parseFloat(lon), diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts new file mode 100644 index 0000000000000..7d99d3635106d --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +function generateUniqueKey() { + return uuid.v4().replace(/-/g, ''); +} + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); + const supertest = getService('supertest'); + const find = getService('find'); + const retry = getService('retry'); + + async function getAlertsByName(name: string) { + const { + body: { data: alerts }, + } = await supertest.get(`/api/alerts/_find?search=${name}&search_fields=name`).expect(200); + + return alerts; + } + + async function deleteAlerts(alertIds: string[]) { + alertIds.forEach(async (alertId: string) => { + await supertest.delete(`/api/alerts/alert/${alertId}`).set('kbn-xsrf', 'foo').expect(204, ''); + }); + } + + async function defineAlert(alertName: string) { + await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await testSubjects.setValue('alertNameInput', alertName); + await testSubjects.click('.index-threshold-SelectOption'); + await testSubjects.click('selectIndexExpression'); + const comboBox = await find.byCssSelector('#indexSelectSearchBox'); + await comboBox.click(); + await comboBox.type('k'); + const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); + await filterSelectItem.click(); + await testSubjects.click('thresholdAlertTimeFieldSelect'); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); + // need this two out of popup clicks to close them + const nameInput = await testSubjects.find('alertNameInput'); + await nameInput.click(); + } + + describe('create alert', function () { + before(async () => { + await pageObjects.common.navigateToApp('triggersActions'); + await testSubjects.click('alertsTab'); + }); + + it('should create an alert', async () => { + const alertName = generateUniqueKey(); + await defineAlert(alertName); + + await testSubjects.click('.slack-ActionTypeSelectOption'); + await testSubjects.click('addNewActionConnectorButton-.slack'); + const slackConnectorName = generateUniqueKey(); + await testSubjects.setValue('nameInput', slackConnectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); + await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); + const createdConnectorToastTitle = await pageObjects.common.closeToast(); + expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`); + await testSubjects.setValue('messageTextArea', 'test message '); + await testSubjects.click('messageAddVariableButton'); + await testSubjects.click('variableMenuButton-0'); + const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]'); + expect(await messageTextArea.getAttribute('value')).to.eql('test message {{alertId}}'); + await messageTextArea.type(' some additional text '); + + await testSubjects.click('messageAddVariableButton'); + await testSubjects.click('variableMenuButton-1'); + + expect(await messageTextArea.getAttribute('value')).to.eql( + 'test message {{alertId}} some additional text {{alertInstanceId}}' + ); + + await testSubjects.click('saveAlertButton'); + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Created alert "${alertName}"`); + await pageObjects.triggersActionsUI.searchAlerts(alertName); + const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList(); + expect(searchResultsAfterSave).to.eql([ + { + name: alertName, + tagsText: '', + alertType: 'Index threshold', + interval: '1m', + }, + ]); + + // clean up created alert + const alertsToDelete = await getAlertsByName(alertName); + await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); + }); + + it('should show save confirmation before creating alert with no actions', async () => { + const alertName = generateUniqueKey(); + await defineAlert(alertName); + + await testSubjects.click('saveAlertButton'); + await testSubjects.existOrFail('confirmAlertSaveModal'); + await testSubjects.click('confirmAlertSaveModal > confirmModalCancelButton'); + await testSubjects.missingOrFail('confirmAlertSaveModal'); + await find.existsByCssSelector('[data-test-subj="saveAlertButton"]:not(disabled)'); + + await testSubjects.click('saveAlertButton'); + await testSubjects.existOrFail('confirmAlertSaveModal'); + await testSubjects.click('confirmAlertSaveModal > confirmModalConfirmButton'); + await testSubjects.missingOrFail('confirmAlertSaveModal'); + + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Created alert "${alertName}"`); + await pageObjects.triggersActionsUI.searchAlerts(alertName); + const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList(); + expect(searchResultsAfterSave).to.eql([ + { + name: alertName, + tagsText: '', + alertType: 'Index threshold', + interval: '1m', + }, + ]); + + // clean up created alert + const alertsToDelete = await getAlertsByName(alertName); + await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts similarity index 66% rename from x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts rename to x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index ce821b56d3a8a..a69db68c7d35e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -14,12 +14,19 @@ function generateUniqueKey() { } export default ({ getPageObjects, getService }: FtrProviderContext) => { + const alerting = getService('alerting'); const testSubjects = getService('testSubjects'); + const find = getService('find'); const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const supertest = getService('supertest'); - const find = getService('find'); const retry = getService('retry'); + async function deleteAlerts(alertIds: string[]) { + alertIds.forEach(async (alertId: string) => { + await supertest.delete(`/api/alerts/alert/${alertId}`).set('kbn-xsrf', 'foo').expect(204, ''); + }); + } + async function createAlert(overwrites: Record = {}) { const { body: createdAlert } = await supertest .post(`/api/alerts/alert`) @@ -40,132 +47,42 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return createdAlert; } - async function getAlertsByName(name: string) { - const { - body: { data: alerts }, - } = await supertest.get(`/api/alerts/_find?search=${name}&search_fields=name`).expect(200); - - return alerts; - } - - async function deleteAlerts(alertIds: string[]) { - alertIds.forEach(async (alertId: string) => { - await supertest.delete(`/api/alerts/alert/${alertId}`).set('kbn-xsrf', 'foo').expect(204, ''); - }); + async function createFailingAlert(overwrites: Record = {}) { + const { body: createdAlert } = await supertest + .post(`/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send({ + enabled: true, + name: generateUniqueKey(), + tags: ['foo', 'bar'], + alertTypeId: 'test.failing', + consumer: 'alerts', + schedule: { interval: '30s' }, + throttle: '1m', + actions: [], + params: {}, + ...overwrites, + }) + .expect(200); + return createdAlert; } - async function defineAlert(alertName: string) { - await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('alertNameInput', alertName); - await testSubjects.click('.index-threshold-SelectOption'); - await testSubjects.click('selectIndexExpression'); - const comboBox = await find.byCssSelector('#indexSelectSearchBox'); - await comboBox.click(); - await comboBox.type('k'); - const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); - await filterSelectItem.click(); - await testSubjects.click('thresholdAlertTimeFieldSelect'); - await retry.try(async () => { - const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); - expect(fieldOptions[1]).not.to.be(undefined); - await fieldOptions[1].click(); - }); - await testSubjects.click('closePopover'); - // need this two out of popup clicks to close them - const nameInput = await testSubjects.find('alertNameInput'); - await nameInput.click(); + async function refreshAlertsList() { + await testSubjects.click('alertsTab'); } - describe('alerts', function () { + describe('alerts list', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('alertsTab'); }); - it('should create an alert', async () => { - const alertName = generateUniqueKey(); - await defineAlert(alertName); - - await testSubjects.click('.slack-ActionTypeSelectOption'); - await testSubjects.click('addNewActionConnectorButton-.slack'); - const slackConnectorName = generateUniqueKey(); - await testSubjects.setValue('nameInput', slackConnectorName); - await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); - await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); - const createdConnectorToastTitle = await pageObjects.common.closeToast(); - expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`); - await testSubjects.setValue('messageTextArea', 'test message '); - await testSubjects.click('messageAddVariableButton'); - await testSubjects.click('variableMenuButton-0'); - const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]'); - expect(await messageTextArea.getAttribute('value')).to.eql('test message {{alertId}}'); - await messageTextArea.type(' some additional text '); - - await testSubjects.click('messageAddVariableButton'); - await testSubjects.click('variableMenuButton-1'); - - expect(await messageTextArea.getAttribute('value')).to.eql( - 'test message {{alertId}} some additional text {{alertInstanceId}}' - ); - - await testSubjects.click('saveAlertButton'); - const toastTitle = await pageObjects.common.closeToast(); - expect(toastTitle).to.eql(`Created alert "${alertName}"`); - await pageObjects.triggersActionsUI.searchAlerts(alertName); - const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResultsAfterSave).to.eql([ - { - name: alertName, - tagsText: '', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - - // clean up created alert - const alertsToDelete = await getAlertsByName(alertName); - await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); - }); - - it('should show save confirmation before creating alert with no actions', async () => { - const alertName = generateUniqueKey(); - await defineAlert(alertName); - - await testSubjects.click('saveAlertButton'); - await testSubjects.existOrFail('confirmAlertSaveModal'); - await testSubjects.click('confirmAlertSaveModal > confirmModalCancelButton'); - await testSubjects.missingOrFail('confirmAlertSaveModal'); - await find.existsByCssSelector('[data-test-subj="saveAlertButton"]:not(disabled)'); - - await testSubjects.click('saveAlertButton'); - await testSubjects.existOrFail('confirmAlertSaveModal'); - await testSubjects.click('confirmAlertSaveModal > confirmModalConfirmButton'); - await testSubjects.missingOrFail('confirmAlertSaveModal'); - - const toastTitle = await pageObjects.common.closeToast(); - expect(toastTitle).to.eql(`Created alert "${alertName}"`); - await pageObjects.triggersActionsUI.searchAlerts(alertName); - const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResultsAfterSave).to.eql([ - { - name: alertName, - tagsText: '', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - - // clean up created alert - const alertsToDelete = await getAlertsByName(alertName); - await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); - }); - it('should display alerts in alphabetical order', async () => { const uniqueKey = generateUniqueKey(); const a = await createAlert({ name: 'b', tags: [uniqueKey] }); const b = await createAlert({ name: 'c', tags: [uniqueKey] }); const c = await createAlert({ name: 'a', tags: [uniqueKey] }); - + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(uniqueKey); const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); @@ -179,6 +96,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should search for alert', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); @@ -195,6 +113,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should search for tags', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(`${createdAlert.name} foo`); const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); @@ -210,13 +129,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should display an empty list when search did not return any alerts', async () => { + const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(`An Alert That For Sure Doesn't Exist!`); expect(await pageObjects.triggersActionsUI.isAlertsListDisplayed()).to.eql(true); + await deleteAlerts([createdAlert.id]); }); it('should disable single alert', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); @@ -235,6 +158,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should re-enable single alert', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); @@ -259,6 +183,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should mute single alert', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); @@ -277,6 +202,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should unmute single alert', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); @@ -302,6 +228,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should delete single alert', async () => { const firstAlert = await createAlert(); const secondAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(secondAlert.name); await testSubjects.click('collapsedItemActions'); @@ -324,6 +251,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should mute all selection', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click(`checkboxSelectRow-${createdAlert.id}`); @@ -347,6 +275,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should unmute all selection', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click(`checkboxSelectRow-${createdAlert.id}`); @@ -372,6 +301,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should disable all selection', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click(`checkboxSelectRow-${createdAlert.id}`); @@ -395,6 +325,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should enable all selection', async () => { const createdAlert = await createAlert(); + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click(`checkboxSelectRow-${createdAlert.id}`); @@ -424,7 +355,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const createdAlertsFirstPage = await Promise.all( times(2, () => createAlert({ name: `${namePrefix}-0${count++}` })) ); - + await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts(namePrefix); for (const createdAlert of createdAlertsFirstPage) { @@ -447,5 +378,140 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const searchResultsAfterDelete = await pageObjects.triggersActionsUI.getAlertsList(); expect(searchResultsAfterDelete).to.have.length(0); }); + + it('should filter alerts by the status', async () => { + const createdAlert = await createAlert(); + const failinfAlert = await createFailingAlert(); + // initialy alert get Pending status, so we need to retry refresh list logic to get the post execution statuses + await retry.try(async () => { + await refreshAlertsList(); + const refreshResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); + expect(refreshResults.map((item) => item.status).sort()).to.eql(['Error', 'Ok']); + }); + await testSubjects.click('alertStatusFilterButton'); + await testSubjects.click('alertStatuserrorFilerOption'); // select Error status filter + await retry.try(async () => { + const filterErrorOnlyResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); + expect(filterErrorOnlyResults).to.eql([ + { + name: failinfAlert.name, + tagsText: 'foo, bar', + alertType: 'Test: Failing', + interval: '30s', + status: 'Error', + }, + ]); + }); + + await deleteAlerts([createdAlert.id, failinfAlert.id]); + }); + + it('should display total alerts by status and error banner only when exists alerts with status error', async () => { + const createdAlert = await createAlert(); + await retry.try(async () => { + await refreshAlertsList(); + const refreshResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); + expect(refreshResults).to.eql([ + { + name: createdAlert.name, + tagsText: 'foo, bar', + alertType: 'Test: Noop', + interval: '1m', + status: 'Ok', + }, + ]); + }); + + const alertsErrorBannerWhenNoErrors = await find.allByCssSelector( + '[data-test-subj="alertsErrorBanner"]' + ); + expect(alertsErrorBannerWhenNoErrors).to.have.length(0); + + const failingAlert = await createFailingAlert(); + await retry.try(async () => { + await refreshAlertsList(); + const alertsErrorBannerExistErrors = await find.allByCssSelector( + '[data-test-subj="alertsErrorBanner"]' + ); + expect(alertsErrorBannerExistErrors).to.have.length(1); + expect( + await ( + await alertsErrorBannerExistErrors[0].findByCssSelector('.euiCallOutHeader') + ).getVisibleText() + ).to.equal('Error found in 1 alert.'); + }); + + expect(await testSubjects.getVisibleText('totalAlertsCount')).to.be( + 'Showing: 2 of 2 alerts.' + ); + expect(await testSubjects.getVisibleText('totalActiveAlertsCount')).to.be('Active: 0'); + expect(await testSubjects.getVisibleText('totalOkAlertsCount')).to.be('Ok: 1'); + expect(await testSubjects.getVisibleText('totalErrorAlertsCount')).to.be('Error: 1'); + expect(await testSubjects.getVisibleText('totalPendingAlertsCount')).to.be('Pending: 0'); + expect(await testSubjects.getVisibleText('totalUnknownAlertsCount')).to.be('Unknown: 0'); + + await deleteAlerts([createdAlert.id, failingAlert.id]); + }); + + it('should filter alerts by the alert type', async () => { + const noopAlert = await createAlert(); + const failinfAlert = await createFailingAlert(); + await refreshAlertsList(); + await testSubjects.click('alertTypeFilterButton'); + await testSubjects.click('alertTypetest.failingFilterOption'); + + await retry.try(async () => { + const filterFailingAlertOnlyResults = await pageObjects.triggersActionsUI.getAlertsList(); + expect(filterFailingAlertOnlyResults).to.eql([ + { + name: failinfAlert.name, + tagsText: 'foo, bar', + alertType: 'Test: Failing', + interval: '30s', + }, + ]); + }); + + await deleteAlerts([noopAlert.id, failinfAlert.id]); + }); + + it('should filter alerts by the action type', async () => { + const noopAlert = await createAlert(); + const action = await alerting.actions.createAction({ + name: `slack-${Date.now()}`, + actionTypeId: '.slack', + config: {}, + secrets: { + webhookUrl: 'https://test', + }, + }); + const noopAlertWithAction = await createAlert({ + actions: [ + { + id: action.id, + actionTypeId: '.slack', + group: 'default', + params: { level: 'info', message: 'gfghfhg' }, + }, + ], + }); + await refreshAlertsList(); + await testSubjects.click('actionTypeFilterButton'); + await testSubjects.click('actionType.slackFilterOption'); + + await retry.try(async () => { + const filterWithSlackOnlyResults = await pageObjects.triggersActionsUI.getAlertsList(); + expect(filterWithSlackOnlyResults).to.eql([ + { + name: noopAlertWithAction.name, + tagsText: 'foo, bar', + alertType: 'Test: Noop', + interval: '1m', + }, + ]); + }); + + await deleteAlerts([noopAlertWithAction.id, noopAlert.id]); + }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 3b93607832670..bd799947256d6 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -23,7 +23,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await log.debug('Checking for section heading to say Triggers and Actions.'); const headingText = await pageObjects.triggersActionsUI.getSectionHeadingText(); - expect(headingText).to.be('Alerts and Actions BETA'); + expect(headingText).to.be('Alerts and Actions'); }); describe('Connectors tab', () => { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index 3292184b36056..8600cb6c852f5 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -11,7 +11,8 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { this.tags('ciGroup10'); loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./connectors')); - loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./alerts_list')); + loadTestFile(require.resolve('./alert_create_flyout')); loadTestFile(require.resolve('./details')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index fd7869eac918f..e3927f6bfffb9 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -21,16 +21,17 @@ export class AlertingFixturePlugin implements Plugin { + return getRowItemData(row, $); + }); + }, + async getAlertsListWithStatus() { + const table = await find.byCssSelector('[data-test-subj="alertsList"] table'); + const $ = await table.parseDomContent(); + return $.findTestSubjects('alert-row') + .toArray() + .map((row) => { + const rowItem = getRowItemData(row, $); return { - name: $(row) - .findTestSubject('alertsTableCell-name') - .find('.euiTableCellContent') - .text(), - tagsText: $(row) - .findTestSubject('alertsTableCell-tagsText') - .find('.euiTableCellContent') - .text(), - alertType: $(row) - .findTestSubject('alertsTableCell-alertType') - .find('.euiTableCellContent') - .text(), - interval: $(row) - .findTestSubject('alertsTableCell-interval') + ...rowItem, + status: $(row) + .findTestSubject('alertsTableCell-status') .find('.euiTableCellContent') .text(), }; diff --git a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts new file mode 100644 index 0000000000000..d99e6a69c5124 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy_with_agents_setup.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { setupIngest, getSupertestWithoutAuth } from '../fleet/agents/services'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); + const kibanaServer = getService('kibanaServer'); + + async function getEnrollmentKeyForPolicyId(policyId: string) { + const listRes = await supertest.get(`/api/fleet/enrollment-api-keys`).expect(200); + + const key = listRes.body.list.find( + (item: { policy_id: string; id: string }) => item.policy_id === policyId + ); + + expect(key).not.empty(); + + const res = await supertest.get(`/api/fleet/enrollment-api-keys/${key.id}`).expect(200); + + return res.body.item; + } + + // Enroll an agent to get the actions for an agent as encrypted saved object are not expose otherwise + async function getAgentActionsForEnrollmentKey(enrollmentAPIToken: string) { + const kibanaVersionAccessor = kibanaServer.version; + const kibanaVersion = await kibanaVersionAccessor.get(); + + const { body: enrollmentResponse } = await supertestWithoutAuth + .post(`/api/ingest_manager/fleet/agents/enroll`) + .set('kbn-xsrf', 'xxx') + .set('Authorization', `ApiKey ${enrollmentAPIToken}`) + .send({ + type: 'PERMANENT', + metadata: { + local: { + elastic: { agent: { version: kibanaVersion } }, + }, + user_provided: {}, + }, + }) + .expect(200); + + const agentAccessAPIKey = enrollmentResponse.item.access_api_key; + + // Agent checkin + const { body: checkinApiResponse } = await supertestWithoutAuth + .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/checkin`) + .set('kbn-xsrf', 'xx') + .set('Authorization', `ApiKey ${agentAccessAPIKey}`) + .send({ + events: [], + }) + .expect(200); + + expect(checkinApiResponse.actions).length(1); + + return checkinApiResponse.actions[0]; + } + + // Test all the side effect that should occurs when we create|update an agent policy + describe('ingest_manager_agent_policies_with_agents_setup', () => { + skipIfNoDockerRegistry(providerContext); + + before(async () => { + await esArchiver.loadIfNeeded('fleet/agents'); + }); + after(async () => { + await esArchiver.unload('fleet/agents'); + }); + + setupIngest(providerContext); + + describe('POST /api/fleet/agent_policies', () => { + it('should create an enrollment key and an agent action `POLICY_CHANGE` for the policy', async () => { + const name = `test-${Date.now()}`; + + const res = await supertest + .post(`/api/fleet/agent_policies?sys_monitoring=true`) + .set('kbn-xsrf', 'xxxx') + .send({ + name, + namespace: 'default', + }) + .expect(200); + + const policyId = res.body.item.id; + const enrollmentKey = await getEnrollmentKeyForPolicyId(policyId); + expect(enrollmentKey).not.empty(); + + const action = await getAgentActionsForEnrollmentKey(enrollmentKey.api_key); + + expect(action.type).to.be('POLICY_CHANGE'); + const agentPolicy = action.data.policy; + expect(agentPolicy.id).to.be(policyId); + // should have system inputs + expect(agentPolicy.inputs).length(2); + // should have default output + expect(agentPolicy.outputs.default).not.empty(); + }); + }); + + describe('POST /api/fleet/agent_policies/copy', () => { + const TEST_POLICY_ID = `policy1`; + + it('should create an enrollment key and an agent action `POLICY_CHANGE` for the policy', async () => { + const name = `test-${Date.now()}`; + + const res = await supertest + .post(`/api/fleet/agent_policies/${TEST_POLICY_ID}/copy`) + .set('kbn-xsrf', 'xxxx') + .send({ + name, + description: 'Test', + }) + .expect(200); + + const policyId = res.body.item.id; + const enrollmentKey = await getEnrollmentKeyForPolicyId(policyId); + expect(enrollmentKey).not.empty(); + + const action = await getAgentActionsForEnrollmentKey(enrollmentKey.api_key); + expect(action.type).to.be('POLICY_CHANGE'); + expect(action.data.policy.id).to.be(policyId); + }); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/index.js b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/index.js index a513e7991fa74..da608b7057c0a 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/index.js @@ -6,6 +6,7 @@ export default function loadTests({ loadTestFile }) { describe('Ingest Manager Endpoints', () => { + loadTestFile(require.resolve('./agent_policy_with_agents_setup')); loadTestFile(require.resolve('./agent_policy')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts index d1d909f773a2b..3dbfdfb45008f 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts @@ -65,7 +65,7 @@ export default function (providerContext: FtrProviderContext) { }); }); afterEach(async () => { - if (!server) return; + if (!server.enabled) return; await es.transport.request({ method: 'DELETE', path: `/_data_stream/${logsTemplateName}-default`, diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_multiple.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_multiple.ts new file mode 100644 index 0000000000000..82072f59a482b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_multiple.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); + const pkgName = 'all_assets'; + const pkgVersion = '0.1.0'; + const pkgKey = `${pkgName}-${pkgVersion}`; + const experimentalPkgName = 'experimental'; + const experimentalPkgKey = `${experimentalPkgName}-${pkgVersion}`; + const experimental2PkgName = 'experimental2'; + const experimental2PkgKey = `${experimental2PkgName}-${pkgVersion}`; + + const uninstallPackage = async (pkg: string) => { + await supertest.delete(`/api/fleet/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); + }; + const installPackage = async (pkg: string) => { + await supertest + .post(`/api/fleet/epm/packages/${pkg}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }); + }; + + const installPackages = async (pkgs: string[]) => { + const installingPackagesPromise = pkgs.map((pkg) => installPackage(pkg)); + return Promise.all(installingPackagesPromise); + }; + const uninstallPackages = async (pkgs: string[]) => { + const uninstallingPackagesPromise = pkgs.map((pkg) => uninstallPackage(pkg)); + return Promise.all(uninstallingPackagesPromise); + }; + const expectPkgFieldToExist = async ( + fields: any[], + fieldName: string, + exists: boolean = true + ) => { + const fieldExists = fields.find((field: { name: string }) => field.name === fieldName); + if (exists) { + expect(fieldExists).not.to.be(undefined); + } else { + expect(fieldExists).to.be(undefined); + } + }; + describe('installs and uninstalls multiple packages side effects', async () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + if (!server.enabled) return; + await installPackages([pkgKey, experimentalPkgKey, experimental2PkgKey]); + }); + after(async () => { + if (!server.enabled) return; + await uninstallPackages([pkgKey, experimentalPkgKey, experimental2PkgKey]); + }); + it('should create index patterns from all installed packages, experimental or beta', async () => { + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + + const fieldsLogs = JSON.parse(resIndexPatternLogs.attributes.fields); + + expectPkgFieldToExist(fieldsLogs, 'logs_test_name'); + expectPkgFieldToExist(fieldsLogs, 'logs_experimental_name'); + expectPkgFieldToExist(fieldsLogs, 'logs_experimental2_name'); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); + expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); + expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); + expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental2_name'); + }); + it('should correctly recreate index patterns when a package is uninstalled', async () => { + await uninstallPackage(experimental2PkgKey); + const resIndexPatternLogs = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'logs-*', + }); + const fields = JSON.parse(resIndexPatternLogs.attributes.fields); + expectPkgFieldToExist(fields, 'logs_test_name'); + expectPkgFieldToExist(fields, 'logs_experimental_name'); + expectPkgFieldToExist(fields, 'logs_experimental2_name', false); + const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); + + expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); + expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); + expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental2_name', false); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts index 6fd4b64f0ee5e..2e7ab199a7fbc 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts @@ -16,6 +16,8 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); const pkgName = 'multiple_versions'; const pkgVersion = '0.1.0'; const pkgUpdateVersion = '0.2.0'; @@ -23,6 +25,7 @@ export default function (providerContext: FtrProviderContext) { skipIfNoDockerRegistry(providerContext); describe('package install', async () => { before(async () => { + if (!server.enabled) return; await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') @@ -84,6 +87,7 @@ export default function (providerContext: FtrProviderContext) { expect(packageAfterSetup.attributes.install_status).equal('installing'); }); after(async () => { + if (!server.enabled) return; await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) .set('kbn-xsrf', 'xxxx') @@ -92,6 +96,7 @@ export default function (providerContext: FtrProviderContext) { }); describe('package update', async () => { before(async () => { + if (!server.enabled) return; await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') @@ -164,6 +169,7 @@ export default function (providerContext: FtrProviderContext) { expect(packageAfterSetup.attributes.version).equal(pkgVersion); }); after(async () => { + if (!server.enabled) return; await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) .set('kbn-xsrf', 'xxxx') diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/ecs.yml new file mode 100644 index 0000000000000..4c18291b7c5ad --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: logs_experimental_name + title: logs_experimental_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_logs/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/ecs.yml new file mode 100644 index 0000000000000..3cd2db230f437 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: metrics_experimental_name + title: metrics_experimental_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/manifest.yml new file mode 100644 index 0000000000000..6bc20442bd432 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/data_stream/test_metrics/manifest.yml @@ -0,0 +1,3 @@ +title: Test Dataset + +type: metrics \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/docs/README.md new file mode 100644 index 0000000000000..8e524c4c71b5f --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing side effects when installing and removing multiple packages diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/img/logo_overrides_64_color.svg b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/img/logo_overrides_64_color.svg new file mode 100644 index 0000000000000..b03007a76ffcc --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml new file mode 100644 index 0000000000000..9c83569a69cbe --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: experimental +title: experimental integration +description: This is a test package for testing experimental packages +version: 0.1.0 +categories: [] +release: experimental +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/ecs.yml new file mode 100644 index 0000000000000..dad07fa9637af --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: logs_experimental2_name + title: logs_experimental2_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_logs/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/ecs.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/ecs.yml new file mode 100644 index 0000000000000..0b6a2efaacd33 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/ecs.yml @@ -0,0 +1,3 @@ +- name: metrics_experimental2_name + title: metrics_experimental2_title + type: keyword \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/manifest.yml new file mode 100644 index 0000000000000..6bc20442bd432 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/data_stream/test_metrics/manifest.yml @@ -0,0 +1,3 @@ +title: Test Dataset + +type: metrics \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/docs/README.md new file mode 100644 index 0000000000000..8e524c4c71b5f --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing side effects when installing and removing multiple packages diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/img/logo_overrides_64_color.svg b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/img/logo_overrides_64_color.svg new file mode 100644 index 0000000000000..b03007a76ffcc --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/manifest.yml new file mode 100644 index 0000000000000..766835dbde037 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/experimental2/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: experimental2 +title: experimental integration +description: This is a test package for testing experimental packages +version: 0.1.0 +categories: [] +release: experimental +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/plugin_api_integration/config.ts b/x-pack/test/plugin_api_integration/config.ts index b89ed6ad550a3..30a361ea2a379 100644 --- a/x-pack/test/plugin_api_integration/config.ts +++ b/x-pack/test/plugin_api_integration/config.ts @@ -43,6 +43,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { '--xpack.eventLog.enabled=true', '--xpack.eventLog.logEntries=true', '--xpack.eventLog.indexEntries=true', + '--xpack.task_manager.monitored_aggregated_stats_refresh_rate=5000', ...plugins.map( (pluginDir) => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` ), diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts index 803df6a66ea58..b5d2c98d8cbcd 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import { Plugin, CoreSetup, CoreStart } from 'src/core/server'; import { EventEmitter } from 'events'; import { Subject } from 'rxjs'; @@ -103,6 +104,29 @@ export class SampleTaskManagerFixturePlugin // fail after the first failed run maxAttempts: 1, }, + sampleRecurringTaskTimingOut: { + title: 'Sample Recurring Task that Times Out', + description: 'A sample task that times out each run.', + maxAttempts: 3, + timeout: '1s', + createTaskRunner: () => ({ + async run() { + return await new Promise((resolve) => {}); + }, + }), + }, + sampleOneTimeTaskTimingOut: { + title: 'Sample One-Time Task that Times Out', + description: 'A sample task that times out each run.', + maxAttempts: 3, + timeout: '1s', + getRetry: (attempts: number, error: object) => new Date(Date.now() + _.random(2, 5) * 1000), + createTaskRunner: () => ({ + async run() { + return await new Promise((resolve) => {}); + }, + }), + }, }); taskManager.addMiddleware({ diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index eea18863e3be8..dff10daafbdb8 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -157,6 +157,32 @@ export default function ({ getService }: FtrProviderContext) { }); }); } + + describe(`Index Lifecycle`, () => { + it('should query across indicies matching the Event Log index pattern', async () => { + await esArchiver.load('event_log_multiple_indicies'); + + const id = `421f2511-5cd1-44fd-95df-e0df83e354d5`; + + const { + body: { data, total }, + } = await findEvents(undefined, id, {}); + + expect(data.length).to.be(6); + expect(total).to.be(6); + + expect(data.map((foundEvent: IEvent) => foundEvent?.message)).to.eql([ + 'test 2020-10-28T15:19:53.825Z', + 'test 2020-10-28T15:19:54.849Z', + 'test 2020-10-28T15:19:54.881Z', + 'test 2020-10-28T15:19:55.913Z', + 'test 2020-10-28T15:19:55.938Z', + 'test 2020-10-28T15:19:55.962Z', + ]); + + await esArchiver.unload('event_log_multiple_indicies'); + }); + }); }); async function findEvents( diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts new file mode 100644 index 0000000000000..9b02b58573673 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import url from 'url'; +import { keyBy, mapValues } from 'lodash'; +import supertestAsPromised from 'supertest-as-promised'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; + +interface MonitoringStats { + last_update: string; + status: string; + stats: { + configuration: { + timestamp: string; + value: Record; + }; + workload: { + timestamp: string; + value: { + count: number; + task_types: Record; + schedule: Array<[string, number]>; + overdue: number; + estimated_schedule_density: number[]; + }; + }; + runtime: { + timestamp: string; + value: { + drift: Record; + execution: { + duration: Record>; + result_frequency_percent_as_number: Record>; + }; + polling: { + last_successful_poll: string; + result_frequency_percent_as_number: Record; + }; + }; + }; + }; +} + +export default function ({ getService }: FtrProviderContext) { + const config = getService('config'); + const retry = getService('retry'); + const supertest = supertestAsPromised(url.format(config.get('servers.kibana'))); + + function getHealthRequest() { + return supertest.get('/api/task_manager/_health').set('kbn-xsrf', 'foo'); + } + + function getHealth(): Promise { + return getHealthRequest() + .expect(200) + .then((response) => response.body); + } + + function scheduleTask(task: Partial): Promise { + return supertest + .post('/api/sample_tasks/schedule') + .set('kbn-xsrf', 'xxx') + .send({ task }) + .expect(200) + .then((response: { body: ConcreteTaskInstance }) => response.body); + } + + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + const monitoredAggregatedStatsRefreshRate = 5000; + + describe('health', () => { + it('should return basic configuration of task manager', async () => { + const health = await getHealth(); + expect(health.status).to.eql('OK'); + expect(health.stats.configuration.value).to.eql({ + poll_interval: 3000, + max_poll_inactivity_cycles: 10, + monitored_aggregated_stats_refresh_rate: monitoredAggregatedStatsRefreshRate, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + custom: {}, + default: { + error_threshold: 90, + warn_threshold: 80, + }, + }, + request_capacity: 1000, + max_workers: 10, + }); + }); + + it('should return the task manager workload', async () => { + const health = await getHealth(); + const { + status, + stats: { workload }, + } = health; + + expect(status).to.eql('OK'); + + const sumSampleTaskInWorkload = + (workload.value.task_types as { + sampleTask?: { count: number }; + }).sampleTask?.count ?? 0; + const scheduledWorkload = (mapValues( + keyBy(workload.value.schedule as Array<[string, number]>, ([interval, count]) => interval), + ([, count]) => count + ) as unknown) as { '37m': number | undefined; '37s': number | undefined }; + + await scheduleTask({ + taskType: 'sampleTask', + schedule: { interval: '37s' }, + }); + + await scheduleTask({ + taskType: 'sampleTask', + schedule: { interval: '37m' }, + }); + + await retry.try(async () => { + // workload is configured to refresh every 5s in FTs + await delay(monitoredAggregatedStatsRefreshRate); + + const workloadAfterScheduling = (await getHealth()).stats.workload.value; + + expect( + (workloadAfterScheduling.task_types as { sampleTask: { count: number } }).sampleTask.count + ).to.eql(sumSampleTaskInWorkload + 2); + + const schedulesWorkloadAfterScheduling = (mapValues( + keyBy( + workloadAfterScheduling.schedule as Array<[string, number]>, + ([interval]) => interval + ), + ([, count]) => count + ) as unknown) as { + '37m': number; + '37s': number; + }; + expect(schedulesWorkloadAfterScheduling['37s']).to.eql(1 + (scheduledWorkload['37s'] ?? 0)); + expect(schedulesWorkloadAfterScheduling['37m']).to.eql(1 + (scheduledWorkload['37m'] ?? 0)); + }); + }); + + it('should return a breakdown of idleTasks in the task manager workload', async () => { + const { + workload: { value: workload }, + } = (await getHealth()).stats; + + expect(typeof workload.overdue).to.eql('number'); + + expect(Array.isArray(workload.estimated_schedule_density)).to.eql(true); + + // test run with the default poll_interval of 3s and a monitored_aggregated_stats_refresh_rate of 5s, + // so we expect the estimated_schedule_density to span a minute (which means 20 buckets, as 60s / 3s = 20) + expect(workload.estimated_schedule_density.length).to.eql(20); + }); + + it('should return the task manager runtime stats', async () => { + await scheduleTask({ + taskType: 'sampleTask', + schedule: { interval: '5s' }, + }); + + const { + runtime: { + value: { drift, polling, execution }, + }, + } = (await getHealth()).stats; + + expect(isNaN(Date.parse(polling.last_successful_poll as string))).to.eql(false); + expect(typeof polling.result_frequency_percent_as_number.NoTasksClaimed).to.eql('number'); + expect(typeof polling.result_frequency_percent_as_number.RanOutOfCapacity).to.eql('number'); + expect(typeof polling.result_frequency_percent_as_number.PoolFilled).to.eql('number'); + + expect(typeof drift.p50).to.eql('number'); + expect(typeof drift.p90).to.eql('number'); + expect(typeof drift.p95).to.eql('number'); + expect(typeof drift.p99).to.eql('number'); + + expect(typeof execution.duration.sampleTask.p50).to.eql('number'); + expect(typeof execution.duration.sampleTask.p90).to.eql('number'); + expect(typeof execution.duration.sampleTask.p95).to.eql('number'); + expect(typeof execution.duration.sampleTask.p99).to.eql('number'); + + expect(typeof execution.result_frequency_percent_as_number.sampleTask.Success).to.eql( + 'number' + ); + expect(typeof execution.result_frequency_percent_as_number.sampleTask.RetryScheduled).to.eql( + 'number' + ); + expect(typeof execution.result_frequency_percent_as_number.sampleTask.Failed).to.eql( + 'number' + ); + }); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts similarity index 58% rename from x-pack/test/plugin_api_integration/test_suites/task_manager/index.js rename to x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts index 93350ad4d2c80..b542bff3a4aa9 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export default function ({ loadTestFile }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { describe('task_manager', function taskManagerSuite() { this.tags('ciGroup2'); - loadTestFile(require.resolve('./task_manager_integration')); + loadTestFile(require.resolve('./health_route')); + loadTestFile(require.resolve('./task_management')); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts similarity index 78% rename from x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js rename to x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts index c87a5039360b8..f34cb7594d288 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management.ts @@ -8,19 +8,48 @@ import _ from 'lodash'; import expect from '@kbn/expect'; import url from 'url'; import supertestAsPromised from 'supertest-as-promised'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import TaskManagerMapping from '../../../../plugins/task_manager/server/saved_objects/mappings.json'; +import { + DEFAULT_MAX_WORKERS, + DEFAULT_POLL_INTERVAL, +} from '../../../../plugins/task_manager/server/config'; +import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; const { task: { properties: taskManagerIndexMapping }, -} = require('../../../../plugins/task_manager/server/saved_objects/mappings.json'); +} = TaskManagerMapping; -const { - DEFAULT_MAX_WORKERS, - DEFAULT_POLL_INTERVAL, -} = require('../../../../plugins/task_manager/server/config.ts'); +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +export interface RawDoc { + _id: string; + _source: any; + _type?: string; +} +export interface SearchResults { + hits: { + hits: RawDoc[]; + }; +} -export default function ({ getService }) { +type DeprecatedConcreteTaskInstance = Omit & { + interval: string; +}; + +type SerializedConcreteTaskInstance = Omit< + ConcreteTaskInstance, + 'state' | 'params' | 'scheduledAt' | 'startedAt' | 'retryAt' | 'runAt' +> & { + state: State; + params: Params; + scheduledAt: string; + startedAt: string | null; + retryAt: string | null; + runAt: string; +}; + +export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const log = getService('log'); const retry = getService('retry'); @@ -53,14 +82,18 @@ export default function ({ getService }) { } }); - function currentTasks() { + function currentTasks(): Promise<{ + docs: Array>; + }> { return supertest .get('/api/sample_tasks') .expect(200) .then((response) => response.body); } - function currentTask(task) { + function currentTask( + task: string + ): Promise> { return supertest .get(`/api/sample_tasks/task/${task}`) .send({ task }) @@ -69,32 +102,30 @@ export default function ({ getService }) { } function ensureTasksIndexRefreshed() { - return supertest - .get(`/api/ensure_tasks_index_refreshed`) - .send({}) - .expect(200) - .then((response) => response.body); + return supertest.get(`/api/ensure_tasks_index_refreshed`).send({}).expect(200); } - function historyDocs(taskId) { + function historyDocs(taskId?: string): Promise { return es .search({ index: testHistoryIndex, q: taskId ? `taskId:${taskId}` : 'type:task', }) - .then((result) => result.hits.hits); + .then((result: SearchResults) => result.hits.hits); } - function scheduleTask(task) { + function scheduleTask( + task: Partial + ): Promise { return supertest .post('/api/sample_tasks/schedule') .set('kbn-xsrf', 'xxx') .send({ task }) .expect(200) - .then((response) => response.body); + .then((response: { body: SerializedConcreteTaskInstance }) => response.body); } - function runTaskNow(task) { + function runTaskNow(task: { id: string }) { return supertest .post('/api/sample_tasks/run_now') .set('kbn-xsrf', 'xxx') @@ -103,16 +134,16 @@ export default function ({ getService }) { .then((response) => response.body); } - function scheduleTaskIfNotExists(task) { + function scheduleTaskIfNotExists(task: Partial) { return supertest .post('/api/sample_tasks/ensure_scheduled') .set('kbn-xsrf', 'xxx') .send({ task }) .expect(200) - .then((response) => response.body); + .then((response: { body: ConcreteTaskInstance }) => response.body); } - function releaseTasksWaitingForEventToComplete(event) { + function releaseTasksWaitingForEventToComplete(event: string) { return supertest .post('/api/sample_tasks/event') .set('kbn-xsrf', 'xxx') @@ -120,11 +151,17 @@ export default function ({ getService }) { .expect(200); } - function getTaskById(tasks, id) { + function getTaskById( + tasks: Array>, + id: string + ) { return tasks.filter((task) => task.id === id)[0]; } - async function provideParamsToTasksWaitingForParams(taskId, data = {}) { + async function provideParamsToTasksWaitingForParams( + taskId: string, + data: Record = {} + ) { // wait for task to start running and stall on waitForParams await retry.try(async () => { const tasks = (await currentTasks()).docs; @@ -151,7 +188,7 @@ export default function ({ getService }) { await retry.try(async () => { expect((await historyDocs()).length).to.eql(1); - const [task] = (await currentTasks()).docs; + const [task] = (await currentTasks<{ count: number }>()).docs; log.debug(`Task found: ${task.id}`); log.debug(`Task status: ${task.status}`); log.debug(`Task state: ${JSON.stringify(task.state, null, 2)}`); @@ -236,7 +273,7 @@ export default function ({ getService }) { await retry.try(async () => { expect((await historyDocs(originalTask.id)).length).to.eql(1); - const [task] = (await currentTasks()).docs; + const [task] = (await currentTasks<{ count: number }>()).docs; expect(task.attempts).to.eql(0); expect(task.state.count).to.eql(count + 1); @@ -257,7 +294,7 @@ export default function ({ getService }) { await retry.try(async () => { expect((await historyDocs()).length).to.eql(1); - const [task] = (await currentTasks()).docs; + const [task] = (await currentTasks<{ count: number }>()).docs; expect(task.attempts).to.eql(0); expect(task.state.count).to.eql(1); @@ -278,7 +315,7 @@ export default function ({ getService }) { await retry.try(async () => { expect((await historyDocs()).length).to.eql(1); - const [task] = (await currentTasks()).docs; + const [task] = (await currentTasks<{ count: number }>()).docs; expect(task.attempts).to.eql(0); expect(task.state.count).to.eql(1); @@ -299,7 +336,7 @@ export default function ({ getService }) { 1 ); - const [task] = (await currentTasks()).docs.filter( + const [task] = (await currentTasks<{ count: number }>()).docs.filter( (taskDoc) => taskDoc.id === originalTask.id ); @@ -322,7 +359,7 @@ export default function ({ getService }) { .length ).to.eql(2); - const [task] = (await currentTasks()).docs.filter( + const [task] = (await currentTasks<{ count: number }>()).docs.filter( (taskDoc) => taskDoc.id === originalTask.id ); expect(task.state.count).to.eql(2); @@ -343,7 +380,7 @@ export default function ({ getService }) { const docs = await historyDocs(originalTask.id); expect(docs.length).to.eql(1); - const task = await currentTask(originalTask.id); + const task = await currentTask<{ count: number }>(originalTask.id); expect(task.state.count).to.eql(1); @@ -393,16 +430,16 @@ export default function ({ getService }) { expect(await runNowResult).to.eql({ id: originalTask.id }); await retry.try(async () => { - const task = await currentTask(originalTask.id); + const task = await currentTask<{ count: number }>(originalTask.id); expect(task.state.count).to.eql(2); }); // drain tasks, othrwise they'll keep Task Manager stalled await retry.try(async () => { await releaseTasksWaitingForEventToComplete('releaseTheOthers'); - const tasks = (await currentTasks()).docs.filter( - (task) => task.params.originalParams.waitForEvent === 'releaseTheOthers' - ); + const tasks = ( + await currentTasks<{}, { originalParams: { waitForEvent: string } }>() + ).docs.filter((task) => task.params.originalParams.waitForEvent === 'releaseTheOthers'); expect(tasks.length).to.eql(0); }); }); @@ -420,7 +457,7 @@ export default function ({ getService }) { 1 ); - const task = await currentTask(originalTask.id); + const task = await currentTask<{ count: number }>(originalTask.id); expect(task.state.count).to.eql(1); expect(task.status).to.eql('idle'); @@ -437,7 +474,7 @@ export default function ({ getService }) { expect(successfulRunNowResult).to.eql({ id: originalTask.id }); await retry.try(async () => { - const task = await currentTask(originalTask.id); + const task = await currentTask<{ count: number }>(originalTask.id); expect(task.state.count).to.eql(2); expect(task.status).to.eql('idle'); }); @@ -515,7 +552,7 @@ export default function ({ getService }) { // finish first run by emitting 'runNowHasBeenAttempted' event await releaseTasksWaitingForEventToComplete('runNowHasBeenAttempted'); await retry.try(async () => { - const tasks = (await currentTasks()).docs; + const tasks = (await currentTasks<{ count: number }>()).docs; expect(getTaskById(tasks, longRunningTask.id).state.count).to.eql(1); const task = await currentTask(longRunningTask.id); @@ -564,12 +601,14 @@ export default function ({ getService }) { expect(await runNowResultWithExpectedFailure).to.eql({ id: taskThatFailsBeforeRunNow.id }); }); - async function expectReschedule(originalRunAt, currentTask, expectedDiff) { + async function expectReschedule( + originalRunAt: number, + task: SerializedConcreteTaskInstance, + expectedDiff: number + ) { const buffer = 10000; - expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.greaterThan( - expectedDiff - buffer - ); - expect(Date.parse(currentTask.runAt) - originalRunAt).to.be.lessThan(expectedDiff + buffer); + expect(Date.parse(task.runAt) - originalRunAt).to.be.greaterThan(expectedDiff - buffer); + expect(Date.parse(task.runAt) - originalRunAt).to.be.lessThan(expectedDiff + buffer); } it('should run tasks in parallel, allowing for long running tasks along side faster tasks', async () => { @@ -594,18 +633,47 @@ export default function ({ getService }) { }); await retry.try(async () => { - const tasks = (await currentTasks()).docs; + const tasks = (await currentTasks<{ count: number }>()).docs; expect(getTaskById(tasks, fastTask.id).state.count).to.eql(2); }); await releaseTasksWaitingForEventToComplete('rescheduleHasHappened'); await retry.try(async () => { - const tasks = (await currentTasks()).docs; + const tasks = (await currentTasks<{ count: number }>()).docs; expect(getTaskById(tasks, fastTask.id).state.count).to.greaterThan(2); expect(getTaskById(tasks, longRunningTask.id).state.count).to.eql(1); }); }); + + it('should mark non-recurring task as failed if task is still running but maxAttempts has been reached', async () => { + const task = await scheduleTask({ + taskType: 'sampleOneTimeTaskTimingOut', + params: {}, + }); + + await retry.try(async () => { + const [scheduledTask] = (await currentTasks()).docs; + expect(scheduledTask.id).to.eql(task.id); + expect(scheduledTask.status).to.eql('failed'); + expect(scheduledTask.attempts).to.eql(3); + }); + }); + + it('should continue claiming recurring task even if maxAttempts has been reached', async () => { + const task = await scheduleTask({ + taskType: 'sampleRecurringTaskTimingOut', + schedule: { interval: '1s' }, + params: {}, + }); + + await retry.try(async () => { + const [scheduledTask] = (await currentTasks()).docs; + expect(scheduledTask.id).to.eql(task.id); + expect(scheduledTask.status).to.eql('claiming'); + expect(scheduledTask.attempts).to.be.greaterThan(3); + }); + }); }); } diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index e7d96023f3653..37d35662eb15b 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -59,7 +59,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { apps: { ...xpackFunctionalConfig.get('apps'), resolverTest: { - pathname: '/app/resolver_test', + pathname: '/app/resolverTest', }, }, diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts index 2b7ae3e576590..005d516e2943c 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - describe('GlobalSearchBar', function () { + // See: https://github.com/elastic/kibana/issues/81397 + describe.skip('GlobalSearchBar', function () { const { common } = getPageObjects(['common']); const find = getService('find'); const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/plugin_functional/test_suites/global_search/index.ts b/x-pack/test/plugin_functional/test_suites/global_search/index.ts index a54e6933be69b..f43e293c30fd6 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/index.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/index.ts @@ -7,7 +7,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('GlobalSearch API', function () { + // See https://github.com/elastic/kibana/issues/81397 + describe.skip('GlobalSearch API', function () { this.tags('ciGroup7'); loadTestFile(require.resolve('./global_search_api')); loadTestFile(require.resolve('./global_search_providers')); diff --git a/x-pack/test/plugin_functional/test_suites/resolver/index.ts b/x-pack/test/plugin_functional/test_suites/resolver/index.ts index 9cc2751a4287d..8cdf54a50bc53 100644 --- a/x-pack/test/plugin_functional/test_suites/resolver/index.ts +++ b/x-pack/test/plugin_functional/test_suites/resolver/index.ts @@ -10,18 +10,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common']); const testSubjects = getService('testSubjects'); - describe('Resolver embeddable test app', function () { + describe('Resolver test app', function () { this.tags('ciGroup7'); beforeEach(async function () { await pageObjects.common.navigateToApp('resolverTest'); }); - it('renders a container div for the embeddable', async function () { - await testSubjects.existOrFail('resolverEmbeddableContainer'); - }); - it('renders resolver', async function () { - await testSubjects.existOrFail('resolverEmbeddable'); + it('renders at least one node, one node-list, one edge line, and graph controls', async function () { + await testSubjects.existOrFail('resolver:node'); + await testSubjects.existOrFail('resolver:node-list'); + await testSubjects.existOrFail('resolver:graph:edgeline'); + await testSubjects.existOrFail('resolver:graph-controls'); }); }); } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts index 1af9ec88df852..b45c082423628 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts @@ -262,11 +262,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load('endpoint/resolver_tree/library_events', { useCreate: true }); await queryBar.setQuery(''); await queryBar.submitQuery(); - const expectedLibraryData = ['329 network', '1 library', '1 library']; + const expectedLibraryData = [ + '1 authentication', + '1 session', + '329 network', + '1 library', + '1 library', + ]; await pageObjects.hosts.navigateToEventsPanel(); await pageObjects.hosts.executeQueryAndOpenResolver( 'event.dataset : endpoint.events.library' ); + // This lines will move the resolver view for clear visibility of the related events. + for (let i = 0; i < 7; i++) { + await (await testSubjects.find('resolver:graph-controls:west-button')).click(); + } await pageObjects.hosts.runNodeEvents(expectedLibraryData); }); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts index 3301217e41a90..988fadbbdfe33 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts @@ -127,6 +127,7 @@ export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrPro expect(EventName).to.equal(linkText); expect(EventName).to.equal(expectedData[i]); } + await testSubjects.click('full-screen'); }, /** * Navigate to Events Panel @@ -146,7 +147,6 @@ export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrPro await queryBar.submitQuery(); await testSubjects.click('full-screen'); await testSubjects.click('investigate-in-resolver-button'); - await testSubjects.click('full-screen'); }, }; } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 382d4c073d41e..5c76a11315a56 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -1,20 +1,14 @@ { "extends": "../tsconfig.base.json", - "include": [ - "mocks.ts", - "typings/**/*", - "plugins/**/*", - "test_utils/**/*", - "tasks/**/*" - ], + "include": ["mocks.ts", "typings/**/*", "plugins/**/*", "test_utils/**/*", "tasks/**/*"], "exclude": [ - "test/**/*", - "plugins/security_solution/cypress/**/*", "plugins/apm/e2e/cypress/**/*", "plugins/apm/scripts/**/*", - "plugins/licensing/**/*", "plugins/global_search/**/*", - "plugins/telemetry_collection_xpack/**/*" + "plugins/licensing/**/*", + "plugins/security_solution/cypress/**/*", + "plugins/telemetry_collection_xpack/**/*", + "test/**/*" ], "compilerOptions": { "paths": { @@ -28,15 +22,18 @@ }, "references": [ { "path": "../src/core/tsconfig.json" }, - { "path": "../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../src/plugins/inspector/tsconfig.json" }, + { "path": "../src/plugins/kibana_legacy/tsconfig.json" }, { "path": "../src/plugins/kibana_react/tsconfig.json" }, - { "path": "./plugins/licensing/tsconfig.json" }, - { "path": "./plugins/global_search/tsconfig.json" }, - { "path": "../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../src/plugins/telemetry/tsconfig.json" }, { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, + { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "../src/plugins/telemetry/tsconfig.json" }, + { "path": "../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../src/plugins/usage_collection/tsconfig.json" }, + { "path": "./plugins/global_search/tsconfig.json" }, + { "path": "./plugins/licensing/tsconfig.json" }, + { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" } ] } diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts deleted file mode 100644 index dd9e0239aeee7..0000000000000 --- a/x-pack/typings/hapi.d.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import 'hapi'; - -import { ActionsPlugin, ActionsClient } from '../plugins/actions/server'; -import { AlertingPlugin, AlertsClient } from '../plugins/alerts/server'; -import { TaskManager } from '../plugins/task_manager/server'; - -declare module 'hapi' { - interface Request { - getActionsClient?: () => ActionsClient; - getAlertsClient?: () => AlertsClient; - } - interface PluginProperties { - actions?: ActionsPlugin; - alerts?: AlertingPlugin; - task_manager?: TaskManager; - } -} diff --git a/x-pack/typings/index.d.ts b/x-pack/typings/index.d.ts index 90e2fa5868744..3168026a8679d 100644 --- a/x-pack/typings/index.d.ts +++ b/x-pack/typings/index.d.ts @@ -22,18 +22,8 @@ declare module '*.svg' { export default content; } -type MethodKeysOf = { - [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; -}[keyof T]; - -type PublicMethodsOf = Pick>; - declare module 'axios/lib/adapters/xhr'; -type Writable = { - -readonly [K in keyof T]: T[K]; -}; - // Storybook references this module. It's @ts-ignored in the codebase but when // built into its dist it strips that out. Add it here to avoid a type checking // error. diff --git a/x-pack/typings/jest.d.ts b/x-pack/typings/jest.d.ts deleted file mode 100644 index 488df5ad92373..0000000000000 --- a/x-pack/typings/jest.d.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; - * you may not use this file except in compliance with the Elastic License. - */ - -type MockedKeys = { [P in keyof T]: jest.Mocked> }; - -type DeeplyMockedKeys = { - [P in keyof T]: T[P] extends (...args: any[]) => any - ? jest.MockInstance, Parameters> - : DeeplyMockedKeys; -} & - T; diff --git a/yarn.lock b/yarn.lock index 18f868440f508..6b77b14f09d42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8153,9 +8153,9 @@ camelize@^1.0.0: integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001097: - version "1.0.30001114" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001114.tgz#2e88119afb332ead5eaa330e332e951b1c4bfea9" - integrity sha512-ml/zTsfNBM+T1+mjglWRPgVsu2L76GAaADKX5f4t0pbhttEp0WMawJsHDYlFkVZkoA+89uvBRrVrEE4oqenzXQ== + version "1.0.30001150" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001150.tgz" + integrity sha512-kiNKvihW0m36UhAFnl7bOAv0i1K1f6wpfVtTF5O5O82XzgtBnb05V0XeV3oZ968vfg2sRNChsHw8ASH2hDfoYQ== capture-exit@^2.0.0: version "2.0.0" @@ -8227,7 +8227,7 @@ chai@3.5.0: deep-eql "^0.1.3" type-detect "^1.0.0" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.1: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -17249,6 +17249,14 @@ jest-serializer@^26.5.0: "@types/node" "*" graceful-fs "^4.2.4" +jest-silent-reporter@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/jest-silent-reporter/-/jest-silent-reporter-0.2.1.tgz#554dd62b800989cdbcfba22bf30a1c0db6ad289c" + integrity sha512-nEO3oOFHtEXFjlRCbJOlvEWA7ZHyyyvMsU4WHuAhinYBOI4PiX1EIbsZfQZ/cxHcYliHBU9zY8bPxMPdBGksYw== + dependencies: + chalk "^2.3.1" + jest-util "^24.0.0" + jest-snapshot@^26.3.0, jest-snapshot@^26.4.2: version "26.4.2" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.4.2.tgz#87d3ac2f2bd87ea8003602fbebd8fcb9e94104f6" @@ -29239,10 +29247,10 @@ xhr@^2.0.1: parse-headers "^2.0.0" xtend "^4.0.0" -xml-crypto@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-1.4.0.tgz#de1cec8cd31cbd689cd90d3d6e8a27d4ae807de7" - integrity sha512-K8FRdRxICVulK4WhiTUcJrRyAIJFPVOqxfurA3x/JlmXBTxy+SkEENF6GeRt7p/rB6WSOUS9g0gXNQw5n+407g== +xml-crypto@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-2.0.0.tgz#54cd268ad9d31930afcf7092cbb664258ca9e826" + integrity sha512-/a04qr7RpONRZHOxROZ6iIHItdsQQjN3sj8lJkYDDss8tAkEaAs0VrFjb3tlhmS5snQru5lTs9/5ISSMdPDHlg== dependencies: xmldom "0.1.27" xpath "0.0.27"