diff --git a/.ci/teamcity/checks/doc_api_changes.sh b/.ci/teamcity/checks/doc_api_changes.sh index 821647a39441c..43b65d4e188ba 100755 --- a/.ci/teamcity/checks/doc_api_changes.sh +++ b/.ci/teamcity/checks/doc_api_changes.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:checkDocApiChanges +checks-reporter-with-killswitch "Check Doc API Changes" \ + node scripts/check_published_api_changes diff --git a/.ci/teamcity/checks/eslint.sh b/.ci/teamcity/checks/eslint.sh new file mode 100755 index 0000000000000..d7282b310f81c --- /dev/null +++ b/.ci/teamcity/checks/eslint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +checks-reporter-with-killswitch "Lint: eslint" \ + node scripts/eslint --no-cache diff --git a/.ci/teamcity/checks/file_casing.sh b/.ci/teamcity/checks/file_casing.sh index 66578a4970fec..5c0815bdd9551 100755 --- a/.ci/teamcity/checks/file_casing.sh +++ b/.ci/teamcity/checks/file_casing.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:checkFileCasing +checks-reporter-with-killswitch "Check File Casing" \ + node scripts/check_file_casing --quiet diff --git a/.ci/teamcity/checks/i18n.sh b/.ci/teamcity/checks/i18n.sh index f269816cf6b95..62ea3fbe9b04d 100755 --- a/.ci/teamcity/checks/i18n.sh +++ b/.ci/teamcity/checks/i18n.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:i18nCheck +checks-reporter-with-killswitch "Check i18n" \ + node scripts/i18n_check --ignore-missing diff --git a/.ci/teamcity/checks/licenses.sh b/.ci/teamcity/checks/licenses.sh index 2baca87074630..136d281647cc5 100755 --- a/.ci/teamcity/checks/licenses.sh +++ b/.ci/teamcity/checks/licenses.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:licenses +checks-reporter-with-killswitch "Check Licenses" \ + node scripts/check_licenses --dev diff --git a/.ci/teamcity/checks/verify_dependency_versions.sh b/.ci/teamcity/checks/sasslint.sh similarity index 51% rename from .ci/teamcity/checks/verify_dependency_versions.sh rename to .ci/teamcity/checks/sasslint.sh index 4c2ddf5ce8612..45b90f6a8034e 100755 --- a/.ci/teamcity/checks/verify_dependency_versions.sh +++ b/.ci/teamcity/checks/sasslint.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:verifyDependencyVersions +checks-reporter-with-killswitch "Lint: sasslint" \ + node scripts/sasslint diff --git a/.ci/teamcity/checks/telemetry.sh b/.ci/teamcity/checks/telemetry.sh index 6413584d2057d..034dd6d647ad3 100755 --- a/.ci/teamcity/checks/telemetry.sh +++ b/.ci/teamcity/checks/telemetry.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:telemetryCheck +checks-reporter-with-killswitch "Check Telemetry Schema" \ + node scripts/telemetry_check diff --git a/.ci/teamcity/checks/test_hardening.sh b/.ci/teamcity/checks/test_hardening.sh index 21ee68e5ade70..5799a0b44133b 100755 --- a/.ci/teamcity/checks/test_hardening.sh +++ b/.ci/teamcity/checks/test_hardening.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:test_hardening +checks-reporter-with-killswitch "Test Hardening" \ + node scripts/test_hardening diff --git a/.ci/teamcity/checks/ts_projects.sh b/.ci/teamcity/checks/ts_projects.sh index 8afc195fee555..9d1c898090def 100755 --- a/.ci/teamcity/checks/ts_projects.sh +++ b/.ci/teamcity/checks/ts_projects.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:checkTsProjects +checks-reporter-with-killswitch "Check TypeScript Projects" \ + node scripts/check_ts_projects diff --git a/.ci/teamcity/checks/type_check.sh b/.ci/teamcity/checks/type_check.sh index da8ae3373d976..d465e8f4c52b4 100755 --- a/.ci/teamcity/checks/type_check.sh +++ b/.ci/teamcity/checks/type_check.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:typeCheck +checks-reporter-with-killswitch "Check Types" \ + node scripts/type_check diff --git a/.ci/teamcity/checks/verify_notice.sh b/.ci/teamcity/checks/verify_notice.sh index 8571e0bbceb13..636dc35555f67 100755 --- a/.ci/teamcity/checks/verify_notice.sh +++ b/.ci/teamcity/checks/verify_notice.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:verifyNotice +checks-reporter-with-killswitch "Verify NOTICE" \ + node scripts/notice --validate diff --git a/.ci/teamcity/default/jest.sh b/.ci/teamcity/default/jest.sh new file mode 100755 index 0000000000000..93ca7f76f3a21 --- /dev/null +++ b/.ci/teamcity/default/jest.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-default-jest + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Jest Unit Tests" \ + node scripts/jest --bail --debug diff --git a/.ci/teamcity/oss/api_integration.sh b/.ci/teamcity/oss/api_integration.sh new file mode 100755 index 0000000000000..37241bdbdc075 --- /dev/null +++ b/.ci/teamcity/oss/api_integration.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-api-integration + +checks-reporter-with-killswitch "API Integration Tests" \ + node scripts/functional_tests --config test/api_integration/config.js --bail --debug diff --git a/.ci/teamcity/oss/jest.sh b/.ci/teamcity/oss/jest.sh new file mode 100755 index 0000000000000..3ba9ab0c31c57 --- /dev/null +++ b/.ci/teamcity/oss/jest.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-jest + +checks-reporter-with-killswitch "OSS Jest Unit Tests" \ + node scripts/jest --ci --verbose diff --git a/.ci/teamcity/oss/jest_integration.sh b/.ci/teamcity/oss/jest_integration.sh new file mode 100755 index 0000000000000..1a23c46c8a2c2 --- /dev/null +++ b/.ci/teamcity/oss/jest_integration.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-jest-integration + +checks-reporter-with-killswitch "OSS Jest Integration Tests" \ + node scripts/jest_integration --verbose diff --git a/.ci/teamcity/oss/plugin_functional.sh b/.ci/teamcity/oss/plugin_functional.sh index 41ff549945c0b..5d1ecbcbd48ee 100755 --- a/.ci/teamcity/oss/plugin_functional.sh +++ b/.ci/teamcity/oss/plugin_functional.sh @@ -13,6 +13,6 @@ if [[ ! -d "target" ]]; then fi cd - -yarn run grunt run:pluginFunctionalTestsRelease --from=source -yarn run grunt run:exampleFunctionalTestsRelease --from=source -yarn run grunt run:interpreterFunctionalTestsRelease +./test/scripts/test/plugin_functional.sh +./test/scripts/test/example_functional.sh +./test/scripts/test/interpreter_functional.sh diff --git a/.ci/teamcity/oss/server_integration.sh b/.ci/teamcity/oss/server_integration.sh new file mode 100755 index 0000000000000..ddeef77907c49 --- /dev/null +++ b/.ci/teamcity/oss/server_integration.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${0}")/../util.sh" + +export JOB=kibana-oss-server-integration +export KIBANA_INSTALL_DIR="$PARENT_DIR/build/kibana-build-oss" + +checks-reporter-with-killswitch "Server integration tests" \ + node scripts/functional_tests \ + --config test/server_integration/http/ssl/config.js \ + --config test/server_integration/http/ssl_redirect/config.js \ + --config test/server_integration/http/platform/config.ts \ + --config test/server_integration/http/ssl_with_p12/config.js \ + --config test/server_integration/http/ssl_with_p12_intermediate/config.js \ + --bail \ + --debug \ + --kibana-install-dir $KIBANA_INSTALL_DIR diff --git a/.ci/teamcity/tests/mocha.sh b/.ci/teamcity/tests/mocha.sh index ea6c43c39e397..acb088220fa78 100755 --- a/.ci/teamcity/tests/mocha.sh +++ b/.ci/teamcity/tests/mocha.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:mocha +checks-reporter-with-killswitch "Mocha Tests" \ + node scripts/mocha diff --git a/.ci/teamcity/tests/test_hardening.sh b/.ci/teamcity/tests/test_hardening.sh deleted file mode 100755 index 21ee68e5ade70..0000000000000 --- a/.ci/teamcity/tests/test_hardening.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source "$(dirname "${0}")/../util.sh" - -yarn run grunt run:test_hardening diff --git a/.ci/teamcity/tests/test_projects.sh b/.ci/teamcity/tests/test_projects.sh index 3feaa821424e1..2553650930392 100755 --- a/.ci/teamcity/tests/test_projects.sh +++ b/.ci/teamcity/tests/test_projects.sh @@ -4,4 +4,5 @@ set -euo pipefail source "$(dirname "${0}")/../util.sh" -yarn run grunt run:test_projects +checks-reporter-with-killswitch "Test Projects" \ + yarn kbn run test --exclude kibana --oss --skip-kibana-plugins diff --git a/.teamcity/src/builds/Lint.kt b/.teamcity/src/builds/Lint.kt index 0b3b3b013b5ec..d02f1c9038aca 100644 --- a/.teamcity/src/builds/Lint.kt +++ b/.teamcity/src/builds/Lint.kt @@ -17,7 +17,7 @@ object Lint : BuildType({ scriptContent = """ #!/bin/bash - yarn run grunt run:sasslint + ./.ci/teamcity/checks/sasslint.sh """.trimIndent() } @@ -26,7 +26,7 @@ object Lint : BuildType({ scriptContent = """ #!/bin/bash - yarn run grunt run:eslint + ./.ci/teamcity/checks/eslint.sh """.trimIndent() } } diff --git a/.teamcity/src/builds/test/ApiServerIntegration.kt b/.teamcity/src/builds/test/ApiServerIntegration.kt index d595840c879e6..ca58b628cbd22 100644 --- a/.teamcity/src/builds/test/ApiServerIntegration.kt +++ b/.teamcity/src/builds/test/ApiServerIntegration.kt @@ -9,8 +9,8 @@ object ApiServerIntegration : BuildType({ description = "Executes API and Server Integration Tests" steps { - runbld("API Integration", "yarn run grunt run:apiIntegrationTests") - runbld("Server Integration", "yarn run grunt run:serverIntegrationTests") + runbld("API Integration", "./.ci/teamcity/oss/api_integration.sh") + runbld("Server Integration", "./.ci/teamcity/oss/server_integration.sh") } addTestSettings() diff --git a/.teamcity/src/builds/test/Jest.kt b/.teamcity/src/builds/test/Jest.kt index 04217a4e99b1c..c33c9c2678ca4 100644 --- a/.teamcity/src/builds/test/Jest.kt +++ b/.teamcity/src/builds/test/Jest.kt @@ -12,7 +12,7 @@ object Jest : BuildType({ kibanaAgent(8) steps { - runbld("Jest Unit", "yarn run grunt run:test_jest") + runbld("Jest Unit", "./.ci/teamcity/oss/jest.sh") } addTestSettings() diff --git a/.teamcity/src/builds/test/JestIntegration.kt b/.teamcity/src/builds/test/JestIntegration.kt index 9ec1360dcb1d7..7d44e41493b2b 100644 --- a/.teamcity/src/builds/test/JestIntegration.kt +++ b/.teamcity/src/builds/test/JestIntegration.kt @@ -9,7 +9,7 @@ object JestIntegration : BuildType({ description = "Executes Jest Integration Tests" steps { - runbld("Jest Integration", "yarn run grunt run:test_jest_integration") + runbld("Jest Integration", "./.ci/teamcity/oss/jest_integration.sh") } addTestSettings() diff --git a/.teamcity/src/builds/test/QuickTests.kt b/.teamcity/src/builds/test/QuickTests.kt index cca10cc3f2aa2..5b1d2541480ad 100644 --- a/.teamcity/src/builds/test/QuickTests.kt +++ b/.teamcity/src/builds/test/QuickTests.kt @@ -12,7 +12,7 @@ object QuickTests : BuildType({ kibanaAgent(2) val testScripts = mapOf( - "Test Hardening" to ".ci/teamcity/tests/test_hardening.sh", + "Test Hardening" to ".ci/teamcity/checkes/test_hardening.sh", "Test Projects" to ".ci/teamcity/tests/test_projects.sh", "Mocha Tests" to ".ci/teamcity/tests/mocha.sh" ) diff --git a/.teamcity/src/builds/test/XPackJest.kt b/.teamcity/src/builds/test/XPackJest.kt index 1958d39183bae..8246b60823ff9 100644 --- a/.teamcity/src/builds/test/XPackJest.kt +++ b/.teamcity/src/builds/test/XPackJest.kt @@ -12,10 +12,7 @@ object XPackJest : BuildType({ kibanaAgent(16) steps { - runbld("X-Pack Jest Unit", """ - cd x-pack - node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=6 - """.trimIndent()) + runbld("X-Pack Jest Unit", "./.ci/teamcity/default/jest.sh") } addTestSettings() diff --git a/docs/development/core/public/kibana-plugin-core-public.httpsetup.externalurl.md b/docs/development/core/public/kibana-plugin-core-public.httpsetup.externalurl.md new file mode 100644 index 0000000000000..b26c9d371e496 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.httpsetup.externalurl.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [HttpSetup](./kibana-plugin-core-public.httpsetup.md) > [externalUrl](./kibana-plugin-core-public.httpsetup.externalurl.md) + +## HttpSetup.externalUrl property + +Signature: + +```typescript +externalUrl: IExternalUrl; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.httpsetup.md b/docs/development/core/public/kibana-plugin-core-public.httpsetup.md index bb43e9f588a72..b8a99cbb62353 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpsetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpsetup.md @@ -18,6 +18,7 @@ export interface HttpSetup | [anonymousPaths](./kibana-plugin-core-public.httpsetup.anonymouspaths.md) | IAnonymousPaths | APIs for denoting certain paths for not requiring authentication | | [basePath](./kibana-plugin-core-public.httpsetup.basepath.md) | IBasePath | APIs for manipulating the basePath on URL segments. See [IBasePath](./kibana-plugin-core-public.ibasepath.md) | | [delete](./kibana-plugin-core-public.httpsetup.delete.md) | HttpHandler | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | +| [externalUrl](./kibana-plugin-core-public.httpsetup.externalurl.md) | IExternalUrl | | | [fetch](./kibana-plugin-core-public.httpsetup.fetch.md) | HttpHandler | Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | | [get](./kibana-plugin-core-public.httpsetup.get.md) | HttpHandler | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | | [head](./kibana-plugin-core-public.httpsetup.head.md) | HttpHandler | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-core-public.httphandler.md) for options. | diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md new file mode 100644 index 0000000000000..5a598281c7be7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) + +## IExternalUrl interface + +APIs for working with external URLs. + +Signature: + +```typescript +export interface IExternalUrl +``` + +## Methods + +| Method | Description | +| --- | --- | +| [validateUrl(relativeOrAbsoluteUrl)](./kibana-plugin-core-public.iexternalurl.validateurl.md) | Determines if the provided URL is a valid location to send users. Validation is based on the configured allow list in kibana.yml.If the URL is valid, then a URL will be returned. Otherwise, this will return null. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md new file mode 100644 index 0000000000000..466d7cfebf547 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.validateurl.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) > [validateUrl](./kibana-plugin-core-public.iexternalurl.validateurl.md) + +## IExternalUrl.validateUrl() method + +Determines if the provided URL is a valid location to send users. Validation is based on the configured allow list in kibana.yml. + +If the URL is valid, then a URL will be returned. Otherwise, this will return null. + +Signature: + +```typescript +validateUrl(relativeOrAbsoluteUrl: string): URL | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| relativeOrAbsoluteUrl | string | | + +Returns: + +`URL | null` + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.allow.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.allow.md new file mode 100644 index 0000000000000..ec7129a43b99a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.allow.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) > [allow](./kibana-plugin-core-public.iexternalurlpolicy.allow.md) + +## IExternalUrlPolicy.allow property + +Indicates if this policy allows or denies access to the described destination. + +Signature: + +```typescript +allow: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md new file mode 100644 index 0000000000000..5551d52cc1226 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) > [host](./kibana-plugin-core-public.iexternalurlpolicy.host.md) + +## IExternalUrlPolicy.host property + +Optional host describing the external destination. May be combined with `protocol`. Required if `protocol` is not defined. + +Signature: + +```typescript +host?: string; +``` + +## Example + + +```ts +// allows access to all of google.com, using any protocol. +allow: true, +host: 'google.com' + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md new file mode 100644 index 0000000000000..a87dc69d79e23 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) + +## IExternalUrlPolicy interface + +A policy describing whether access to an external destination is allowed. + +Signature: + +```typescript +export interface IExternalUrlPolicy +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [allow](./kibana-plugin-core-public.iexternalurlpolicy.allow.md) | boolean | Indicates if this policy allows or denies access to the described destination. | +| [host](./kibana-plugin-core-public.iexternalurlpolicy.host.md) | string | Optional host describing the external destination. May be combined with protocol. Required if protocol is not defined. | +| [protocol](./kibana-plugin-core-public.iexternalurlpolicy.protocol.md) | string | Optional protocol describing the external destination. May be combined with host. Required if host is not defined. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md new file mode 100644 index 0000000000000..67b9b439a54f6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) > [protocol](./kibana-plugin-core-public.iexternalurlpolicy.protocol.md) + +## IExternalUrlPolicy.protocol property + +Optional protocol describing the external destination. May be combined with `host`. Required if `host` is not defined. + +Signature: + +```typescript +protocol?: string; +``` + +## Example + + +```ts +// allows access to all destinations over the `https` protocol. +allow: true, +protocol: 'https' + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 5f656b9ca510d..a3df5d30137df 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -73,6 +73,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IAnonymousPaths](./kibana-plugin-core-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | | [IBasePath](./kibana-plugin-core-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. | | [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | +| [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) | APIs for working with external URLs. | +| [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. | | [IHttpFetchError](./kibana-plugin-core-public.ihttpfetcherror.md) | | | [IHttpInterceptController](./kibana-plugin-core-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-core-public.httpinterceptor.md). | | [IHttpResponseInterceptorOverrides](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | diff --git a/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md b/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md index 0838572f26f49..a50df950628b3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md +++ b/docs/development/core/server/kibana-plugin-core-server.appenderconfigtype.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig; +export declare type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RollingFileAppenderConfig; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md new file mode 100644 index 0000000000000..8df4db4aa9b5e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlConfig](./kibana-plugin-core-server.iexternalurlconfig.md) + +## IExternalUrlConfig interface + +External Url configuration for use in Kibana. + +Signature: + +```typescript +export interface IExternalUrlConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [policy](./kibana-plugin-core-server.iexternalurlconfig.policy.md) | IExternalUrlPolicy[] | A set of policies describing which external urls are allowed. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.policy.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.policy.md new file mode 100644 index 0000000000000..b5b6f07038076 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlconfig.policy.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlConfig](./kibana-plugin-core-server.iexternalurlconfig.md) > [policy](./kibana-plugin-core-server.iexternalurlconfig.policy.md) + +## IExternalUrlConfig.policy property + +A set of policies describing which external urls are allowed. + +Signature: + +```typescript +readonly policy: IExternalUrlPolicy[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.allow.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.allow.md new file mode 100644 index 0000000000000..e0c140409dcf0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.allow.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) > [allow](./kibana-plugin-core-server.iexternalurlpolicy.allow.md) + +## IExternalUrlPolicy.allow property + +Indicates of this policy allows or denies access to the described destination. + +Signature: + +```typescript +allow: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md new file mode 100644 index 0000000000000..e65de074f1578 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.host.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) > [host](./kibana-plugin-core-server.iexternalurlpolicy.host.md) + +## IExternalUrlPolicy.host property + +Optional host describing the external destination. May be combined with `protocol`. Required if `protocol` is not defined. + +Signature: + +```typescript +host?: string; +``` + +## Example + + +```ts +// allows access to all of google.com, using any protocol. +allow: true, +host: 'google.com' + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md new file mode 100644 index 0000000000000..8e3658a10ed81 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) + +## IExternalUrlPolicy interface + +A policy describing whether access to an external destination is allowed. + +Signature: + +```typescript +export interface IExternalUrlPolicy +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [allow](./kibana-plugin-core-server.iexternalurlpolicy.allow.md) | boolean | Indicates of this policy allows or denies access to the described destination. | +| [host](./kibana-plugin-core-server.iexternalurlpolicy.host.md) | string | Optional host describing the external destination. May be combined with protocol. Required if protocol is not defined. | +| [protocol](./kibana-plugin-core-server.iexternalurlpolicy.protocol.md) | string | Optional protocol describing the external destination. May be combined with host. Required if host is not defined. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md new file mode 100644 index 0000000000000..00c5d05eb0cc4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.iexternalurlpolicy.protocol.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) > [protocol](./kibana-plugin-core-server.iexternalurlpolicy.protocol.md) + +## IExternalUrlPolicy.protocol property + +Optional protocol describing the external destination. May be combined with `host`. Required if `host` is not defined. + +Signature: + +```typescript +protocol?: string; +``` + +## Example + + +```ts +// allows access to all destinations over the `https` protocol. +allow: true, +protocol: 'https' + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 1a4209ff87c5b..269db90c4db9b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -94,6 +94,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [ICspConfig](./kibana-plugin-core-server.icspconfig.md) | CSP configuration for use in Kibana. | | [ICustomClusterClient](./kibana-plugin-core-server.icustomclusterclient.md) | See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | +| [IExternalUrlConfig](./kibana-plugin-core-server.iexternalurlconfig.md) | External Url configuration for use in Kibana. | +| [IExternalUrlPolicy](./kibana-plugin-core-server.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. | | [IKibanaResponse](./kibana-plugin-core-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) execution | | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [ImageValidation](./kibana-plugin-core-server.imagevalidation.md) | | diff --git a/package.json b/package.json index 513d9b907c96c..1295217b4bcbe 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "test:jest": "node scripts/jest", "test:jest_integration": "node scripts/jest_integration", "test:mocha": "node scripts/mocha", - "test:mocha:coverage": "grunt test:mochaCoverage", "test:ftr": "node scripts/functional_tests", "test:ftr:server": "node scripts/functional_tests_server", "test:ftr:runner": "node scripts/functional_test_runner", @@ -85,6 +84,7 @@ "**/@types/hapi__hapi": "^18.2.6", "**/@types/hapi__mimos": "4.1.0", "**/@types/node": "14.14.7", + "**/chokidar": "^3.4.3", "**/cross-fetch/node-fetch": "^2.6.1", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", @@ -175,7 +175,7 @@ "chalk": "^4.1.0", "check-disk-space": "^2.1.0", "cheerio": "0.22.0", - "chokidar": "^3.4.2", + "chokidar": "^3.4.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "color": "1.0.3", @@ -354,7 +354,7 @@ "@cypress/webpack-preprocessor": "^5.4.11", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "24.3.0", + "@elastic/charts": "24.4.0", "@elastic/eslint-config-kibana": "link:packages/elastic-eslint-config-kibana", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", @@ -674,7 +674,6 @@ "grunt-contrib-copy": "^1.0.0", "grunt-contrib-watch": "^1.1.0", "grunt-peg": "^2.0.1", - "grunt-run": "0.8.1", "gulp": "4.0.2", "gulp-babel": "^8.0.0", "gulp-sourcemaps": "2.6.5", diff --git a/packages/kbn-logging/src/appenders.ts b/packages/kbn-logging/src/appenders.ts index 346d3d6dd1068..a82a95b6b0f8a 100644 --- a/packages/kbn-logging/src/appenders.ts +++ b/packages/kbn-logging/src/appenders.ts @@ -35,5 +35,5 @@ export interface Appender { * @internal */ export interface DisposableAppender extends Appender { - dispose: () => void; + dispose: () => void | Promise; } diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index eb9b7a4a35dc7..922159ab555c8 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -59762,11 +59762,11 @@ const os = __webpack_require__(121); const pMap = __webpack_require__(514); const arrify = __webpack_require__(509); const globby = __webpack_require__(515); -const hasGlob = __webpack_require__(711); -const cpFile = __webpack_require__(713); -const junk = __webpack_require__(723); -const pFilter = __webpack_require__(724); -const CpyError = __webpack_require__(726); +const hasGlob = __webpack_require__(715); +const cpFile = __webpack_require__(717); +const junk = __webpack_require__(727); +const pFilter = __webpack_require__(728); +const CpyError = __webpack_require__(730); const defaultOptions = { ignoreJunk: true @@ -60014,8 +60014,8 @@ const fs = __webpack_require__(134); const arrayUnion = __webpack_require__(516); const glob = __webpack_require__(147); const fastGlob = __webpack_require__(518); -const dirGlob = __webpack_require__(704); -const gitignore = __webpack_require__(707); +const dirGlob = __webpack_require__(708); +const gitignore = __webpack_require__(711); const DEFAULT_FILTER = () => false; @@ -60266,11 +60266,11 @@ module.exports.generateTasks = pkg.generateTasks; Object.defineProperty(exports, "__esModule", { value: true }); var optionsManager = __webpack_require__(520); var taskManager = __webpack_require__(521); -var reader_async_1 = __webpack_require__(675); -var reader_stream_1 = __webpack_require__(699); -var reader_sync_1 = __webpack_require__(700); -var arrayUtils = __webpack_require__(702); -var streamUtils = __webpack_require__(703); +var reader_async_1 = __webpack_require__(679); +var reader_stream_1 = __webpack_require__(703); +var reader_sync_1 = __webpack_require__(704); +var arrayUtils = __webpack_require__(706); +var streamUtils = __webpack_require__(707); /** * Synchronous API. */ @@ -60851,16 +60851,16 @@ module.exports.win32 = win32; var util = __webpack_require__(112); var braces = __webpack_require__(527); var toRegex = __webpack_require__(528); -var extend = __webpack_require__(641); +var extend = __webpack_require__(645); /** * Local dependencies */ -var compilers = __webpack_require__(643); -var parsers = __webpack_require__(670); -var cache = __webpack_require__(671); -var utils = __webpack_require__(672); +var compilers = __webpack_require__(647); +var parsers = __webpack_require__(674); +var cache = __webpack_require__(675); +var utils = __webpack_require__(676); var MAX_LENGTH = 1024 * 64; /** @@ -61741,8 +61741,8 @@ var extend = __webpack_require__(551); */ var compilers = __webpack_require__(553); -var parsers = __webpack_require__(566); -var Braces = __webpack_require__(570); +var parsers = __webpack_require__(568); +var Braces = __webpack_require__(572); var utils = __webpack_require__(554); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -64182,7 +64182,7 @@ utils.extend = __webpack_require__(551); utils.flatten = __webpack_require__(558); utils.isObject = __webpack_require__(536); utils.fillRange = __webpack_require__(559); -utils.repeat = __webpack_require__(565); +utils.repeat = __webpack_require__(567); utils.unique = __webpack_require__(550); utils.define = function(obj, key, val) { @@ -64825,9 +64825,9 @@ function flat(arr, res) { var util = __webpack_require__(112); var isNumber = __webpack_require__(560); -var extend = __webpack_require__(551); -var repeat = __webpack_require__(563); -var toRegex = __webpack_require__(564); +var extend = __webpack_require__(563); +var repeat = __webpack_require__(565); +var toRegex = __webpack_require__(566); /** * Return a range of numbers or letters. @@ -65206,6 +65206,66 @@ function isSlowBuffer (obj) { /* 563 */ /***/ (function(module, exports, __webpack_require__) { +"use strict"; + + +var isObject = __webpack_require__(564); + +module.exports = function extend(o/*, objects*/) { + if (!isObject(o)) { o = {}; } + + var len = arguments.length; + for (var i = 1; i < len; i++) { + var obj = arguments[i]; + + if (isObject(obj)) { + assign(o, obj); + } + } + return o; +}; + +function assign(a, b) { + for (var key in b) { + if (hasOwn(b, key)) { + a[key] = b[key]; + } + } +} + +/** + * Returns true if the given `key` is an own property of `obj`. + */ + +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + + +/***/ }), +/* 564 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-extendable + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +module.exports = function isExtendable(val) { + return typeof val !== 'undefined' && val !== null + && (typeof val === 'object' || typeof val === 'function'); +}; + + +/***/ }), +/* 565 */ +/***/ (function(module, exports, __webpack_require__) { + "use strict"; /*! * repeat-string @@ -65280,7 +65340,7 @@ function repeat(str, num) { /***/ }), -/* 564 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65293,7 +65353,7 @@ function repeat(str, num) { -var repeat = __webpack_require__(563); +var repeat = __webpack_require__(565); var isNumber = __webpack_require__(560); var cache = {}; @@ -65581,7 +65641,7 @@ module.exports = toRegexRange; /***/ }), -/* 565 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65606,13 +65666,13 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 566 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(567); +var Node = __webpack_require__(569); var utils = __webpack_require__(554); /** @@ -65973,15 +66033,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 567 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var isObject = __webpack_require__(536); -var define = __webpack_require__(568); -var utils = __webpack_require__(569); +var define = __webpack_require__(570); +var utils = __webpack_require__(571); var ownNames; /** @@ -66472,7 +66532,7 @@ exports = module.exports = Node; /***/ }), -/* 568 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66510,7 +66570,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 569 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67536,16 +67596,16 @@ function assert(val, message) { /***/ }), -/* 570 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var extend = __webpack_require__(551); -var Snapdragon = __webpack_require__(571); +var Snapdragon = __webpack_require__(573); var compilers = __webpack_require__(553); -var parsers = __webpack_require__(566); +var parsers = __webpack_require__(568); var utils = __webpack_require__(554); /** @@ -67647,17 +67707,17 @@ module.exports = Braces; /***/ }), -/* 571 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(572); -var define = __webpack_require__(599); -var Compiler = __webpack_require__(609); -var Parser = __webpack_require__(638); -var utils = __webpack_require__(618); +var Base = __webpack_require__(574); +var define = __webpack_require__(603); +var Compiler = __webpack_require__(613); +var Parser = __webpack_require__(642); +var utils = __webpack_require__(622); var regexCache = {}; var cache = {}; @@ -67828,20 +67888,20 @@ module.exports.Parser = Parser; /***/ }), -/* 572 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(573); -var CacheBase = __webpack_require__(574); -var Emitter = __webpack_require__(575); +var define = __webpack_require__(575); +var CacheBase = __webpack_require__(576); +var Emitter = __webpack_require__(577); var isObject = __webpack_require__(536); -var merge = __webpack_require__(593); -var pascal = __webpack_require__(596); -var cu = __webpack_require__(597); +var merge = __webpack_require__(597); +var pascal = __webpack_require__(600); +var cu = __webpack_require__(601); /** * Optionally define a custom `cache` namespace to use. @@ -68270,7 +68330,7 @@ module.exports.namespace = namespace; /***/ }), -/* 573 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68308,21 +68368,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 574 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var isObject = __webpack_require__(536); -var Emitter = __webpack_require__(575); -var visit = __webpack_require__(576); -var toPath = __webpack_require__(579); -var union = __webpack_require__(580); -var del = __webpack_require__(584); -var get = __webpack_require__(582); -var has = __webpack_require__(589); -var set = __webpack_require__(592); +var Emitter = __webpack_require__(577); +var visit = __webpack_require__(578); +var toPath = __webpack_require__(581); +var union = __webpack_require__(582); +var del = __webpack_require__(588); +var get = __webpack_require__(585); +var has = __webpack_require__(593); +var set = __webpack_require__(596); /** * Create a `Cache` constructor that when instantiated will @@ -68576,7 +68636,7 @@ module.exports.namespace = namespace; /***/ }), -/* 575 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { @@ -68745,7 +68805,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 576 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68758,8 +68818,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(577); -var mapVisit = __webpack_require__(578); +var visit = __webpack_require__(579); +var mapVisit = __webpack_require__(580); module.exports = function(collection, method, val) { var result; @@ -68782,7 +68842,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 577 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68822,14 +68882,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 578 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(577); +var visit = __webpack_require__(579); /** * Map `visit` over an array of objects. @@ -68866,7 +68926,7 @@ function isObject(val) { /***/ }), -/* 579 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68906,16 +68966,16 @@ function filter(arr) { /***/ }), -/* 580 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(552); -var union = __webpack_require__(581); -var get = __webpack_require__(582); -var set = __webpack_require__(583); +var isObject = __webpack_require__(583); +var union = __webpack_require__(584); +var get = __webpack_require__(585); +var set = __webpack_require__(586); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -68943,7 +69003,27 @@ function arrayify(val) { /***/ }), -/* 581 */ +/* 583 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-extendable + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +module.exports = function isExtendable(val) { + return typeof val !== 'undefined' && val !== null + && (typeof val === 'object' || typeof val === 'function'); +}; + + +/***/ }), +/* 584 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68979,7 +69059,7 @@ module.exports = function union(init) { /***/ }), -/* 582 */ +/* 585 */ /***/ (function(module, exports) { /*! @@ -69035,7 +69115,7 @@ function toString(val) { /***/ }), -/* 583 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69049,9 +69129,9 @@ function toString(val) { var split = __webpack_require__(555); -var extend = __webpack_require__(551); +var extend = __webpack_require__(587); var isPlainObject = __webpack_require__(545); -var isObject = __webpack_require__(552); +var isObject = __webpack_require__(583); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69097,7 +69177,47 @@ function isValidKey(key) { /***/ }), -/* 584 */ +/* 587 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isObject = __webpack_require__(583); + +module.exports = function extend(o/*, objects*/) { + if (!isObject(o)) { o = {}; } + + var len = arguments.length; + for (var i = 1; i < len; i++) { + var obj = arguments[i]; + + if (isObject(obj)) { + assign(o, obj); + } + } + return o; +}; + +function assign(a, b) { + for (var key in b) { + if (hasOwn(b, key)) { + a[key] = b[key]; + } + } +} + +/** + * Returns true if the given `key` is an own property of `obj`. + */ + +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + + +/***/ }), +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69111,7 +69231,7 @@ function isValidKey(key) { var isObject = __webpack_require__(536); -var has = __webpack_require__(585); +var has = __webpack_require__(589); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -69136,7 +69256,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 585 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69149,9 +69269,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(586); -var hasValues = __webpack_require__(588); -var get = __webpack_require__(582); +var isObject = __webpack_require__(590); +var hasValues = __webpack_require__(592); +var get = __webpack_require__(585); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -69162,7 +69282,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 586 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69175,7 +69295,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(587); +var isArray = __webpack_require__(591); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -69183,7 +69303,7 @@ module.exports = function isObject(val) { /***/ }), -/* 587 */ +/* 591 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -69194,7 +69314,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 588 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69237,7 +69357,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 589 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69251,8 +69371,8 @@ module.exports = function hasValue(o, noZero) { var isObject = __webpack_require__(536); -var hasValues = __webpack_require__(590); -var get = __webpack_require__(582); +var hasValues = __webpack_require__(594); +var get = __webpack_require__(585); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -69260,7 +69380,7 @@ module.exports = function(val, prop) { /***/ }), -/* 590 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69273,7 +69393,7 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(591); +var typeOf = __webpack_require__(595); var isNumber = __webpack_require__(560); module.exports = function hasValue(val) { @@ -69327,7 +69447,7 @@ module.exports = function hasValue(val) { /***/ }), -/* 591 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(562); @@ -69452,7 +69572,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 592 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69466,9 +69586,9 @@ module.exports = function kindOf(val) { var split = __webpack_require__(555); -var extend = __webpack_require__(551); +var extend = __webpack_require__(587); var isPlainObject = __webpack_require__(545); -var isObject = __webpack_require__(552); +var isObject = __webpack_require__(583); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69514,14 +69634,14 @@ function isValidKey(key) { /***/ }), -/* 593 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(594); -var forIn = __webpack_require__(595); +var isExtendable = __webpack_require__(598); +var forIn = __webpack_require__(599); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -69585,7 +69705,7 @@ module.exports = mixinDeep; /***/ }), -/* 594 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69606,7 +69726,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 595 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69629,7 +69749,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 596 */ +/* 600 */ /***/ (function(module, exports) { /*! @@ -69656,14 +69776,14 @@ module.exports = pascalcase; /***/ }), -/* 597 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(598); +var utils = __webpack_require__(602); /** * Expose class utils @@ -70028,7 +70148,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 598 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70042,10 +70162,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(581); -utils.define = __webpack_require__(599); +utils.union = __webpack_require__(584); +utils.define = __webpack_require__(603); utils.isObj = __webpack_require__(536); -utils.staticExtend = __webpack_require__(606); +utils.staticExtend = __webpack_require__(610); /** @@ -70056,7 +70176,7 @@ module.exports = utils; /***/ }), -/* 599 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70069,7 +70189,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(600); +var isDescriptor = __webpack_require__(604); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -70094,7 +70214,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 600 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70107,9 +70227,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(601); -var isAccessor = __webpack_require__(602); -var isData = __webpack_require__(604); +var typeOf = __webpack_require__(605); +var isAccessor = __webpack_require__(606); +var isData = __webpack_require__(608); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -70123,7 +70243,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 601 */ +/* 605 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -70276,7 +70396,7 @@ function isBuffer(val) { /***/ }), -/* 602 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70289,7 +70409,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(603); +var typeOf = __webpack_require__(607); // accessor descriptor properties var accessor = { @@ -70352,7 +70472,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 603 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(562); @@ -70474,7 +70594,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 604 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70487,7 +70607,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(605); +var typeOf = __webpack_require__(609); // data descriptor properties var data = { @@ -70536,7 +70656,7 @@ module.exports = isDataDescriptor; /***/ }), -/* 605 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(562); @@ -70658,7 +70778,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 606 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70671,8 +70791,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(607); -var define = __webpack_require__(599); +var copy = __webpack_require__(611); +var define = __webpack_require__(603); var util = __webpack_require__(112); /** @@ -70755,15 +70875,15 @@ module.exports = extend; /***/ }), -/* 607 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var typeOf = __webpack_require__(561); -var copyDescriptor = __webpack_require__(608); -var define = __webpack_require__(599); +var copyDescriptor = __webpack_require__(612); +var define = __webpack_require__(603); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -70936,7 +71056,7 @@ module.exports.has = has; /***/ }), -/* 608 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71024,16 +71144,16 @@ function isObject(val) { /***/ }), -/* 609 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(610); -var define = __webpack_require__(599); -var debug = __webpack_require__(612)('snapdragon:compiler'); -var utils = __webpack_require__(618); +var use = __webpack_require__(614); +var define = __webpack_require__(603); +var debug = __webpack_require__(616)('snapdragon:compiler'); +var utils = __webpack_require__(622); /** * Create a new `Compiler` with the given `options`. @@ -71187,7 +71307,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(637); + var sourcemaps = __webpack_require__(641); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -71208,7 +71328,7 @@ module.exports = Compiler; /***/ }), -/* 610 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71221,7 +71341,7 @@ module.exports = Compiler; -var utils = __webpack_require__(611); +var utils = __webpack_require__(615); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -71336,7 +71456,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 611 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71350,7 +71470,7 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(599); +utils.define = __webpack_require__(603); utils.isObject = __webpack_require__(536); @@ -71366,7 +71486,7 @@ module.exports = utils; /***/ }), -/* 612 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71375,14 +71495,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(613); + module.exports = __webpack_require__(617); } else { - module.exports = __webpack_require__(616); + module.exports = __webpack_require__(620); } /***/ }), -/* 613 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71391,7 +71511,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(614); +exports = module.exports = __webpack_require__(618); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -71573,7 +71693,7 @@ function localstorage() { /***/ }), -/* 614 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { @@ -71589,7 +71709,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(615); +exports.humanize = __webpack_require__(619); /** * The currently active debug mode names, and names to skip. @@ -71781,7 +71901,7 @@ function coerce(val) { /***/ }), -/* 615 */ +/* 619 */ /***/ (function(module, exports) { /** @@ -71939,7 +72059,7 @@ function plural(ms, n, name) { /***/ }), -/* 616 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71955,7 +72075,7 @@ var util = __webpack_require__(112); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(614); +exports = module.exports = __webpack_require__(618); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -72134,7 +72254,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(617); + var net = __webpack_require__(621); stream = new net.Socket({ fd: fd, readable: false, @@ -72193,13 +72313,13 @@ exports.enable(load()); /***/ }), -/* 617 */ +/* 621 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 618 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72209,9 +72329,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(551); -exports.SourceMap = __webpack_require__(619); -exports.sourceMapResolve = __webpack_require__(630); +exports.extend = __webpack_require__(587); +exports.SourceMap = __webpack_require__(623); +exports.sourceMapResolve = __webpack_require__(634); /** * Convert backslash in the given string to forward slashes @@ -72254,7 +72374,7 @@ exports.last = function(arr, n) { /***/ }), -/* 619 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -72262,13 +72382,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(620).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(626).SourceMapConsumer; -exports.SourceNode = __webpack_require__(629).SourceNode; +exports.SourceMapGenerator = __webpack_require__(624).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(630).SourceMapConsumer; +exports.SourceNode = __webpack_require__(633).SourceNode; /***/ }), -/* 620 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72278,10 +72398,10 @@ exports.SourceNode = __webpack_require__(629).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(621); -var util = __webpack_require__(623); -var ArraySet = __webpack_require__(624).ArraySet; -var MappingList = __webpack_require__(625).MappingList; +var base64VLQ = __webpack_require__(625); +var util = __webpack_require__(627); +var ArraySet = __webpack_require__(628).ArraySet; +var MappingList = __webpack_require__(629).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -72690,7 +72810,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 621 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72730,7 +72850,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(622); +var base64 = __webpack_require__(626); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -72836,7 +72956,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 622 */ +/* 626 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72909,7 +73029,7 @@ exports.decode = function (charCode) { /***/ }), -/* 623 */ +/* 627 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73332,7 +73452,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 624 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73342,7 +73462,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(623); +var util = __webpack_require__(627); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -73459,7 +73579,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 625 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73469,7 +73589,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(623); +var util = __webpack_require__(627); /** * Determine whether mappingB is after mappingA with respect to generated @@ -73544,7 +73664,7 @@ exports.MappingList = MappingList; /***/ }), -/* 626 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73554,11 +73674,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(623); -var binarySearch = __webpack_require__(627); -var ArraySet = __webpack_require__(624).ArraySet; -var base64VLQ = __webpack_require__(621); -var quickSort = __webpack_require__(628).quickSort; +var util = __webpack_require__(627); +var binarySearch = __webpack_require__(631); +var ArraySet = __webpack_require__(628).ArraySet; +var base64VLQ = __webpack_require__(625); +var quickSort = __webpack_require__(632).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -74632,7 +74752,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 627 */ +/* 631 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74749,7 +74869,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 628 */ +/* 632 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74869,7 +74989,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 629 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74879,8 +74999,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(620).SourceMapGenerator; -var util = __webpack_require__(623); +var SourceMapGenerator = __webpack_require__(624).SourceMapGenerator; +var util = __webpack_require__(627); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -75288,17 +75408,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 630 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(631) -var resolveUrl = __webpack_require__(632) -var decodeUriComponent = __webpack_require__(633) -var urix = __webpack_require__(635) -var atob = __webpack_require__(636) +var sourceMappingURL = __webpack_require__(635) +var resolveUrl = __webpack_require__(636) +var decodeUriComponent = __webpack_require__(637) +var urix = __webpack_require__(639) +var atob = __webpack_require__(640) @@ -75596,7 +75716,7 @@ module.exports = { /***/ }), -/* 631 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -75659,7 +75779,7 @@ void (function(root, factory) { /***/ }), -/* 632 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75677,13 +75797,13 @@ module.exports = resolveUrl /***/ }), -/* 633 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(634) +var decodeUriComponent = __webpack_require__(638) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -75694,7 +75814,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 634 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75795,7 +75915,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 635 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75818,7 +75938,7 @@ module.exports = urix /***/ }), -/* 636 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75832,7 +75952,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 637 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75840,8 +75960,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(599); -var utils = __webpack_require__(618); +var define = __webpack_require__(603); +var utils = __webpack_require__(622); /** * Expose `mixin()`. @@ -75984,19 +76104,19 @@ exports.comment = function(node) { /***/ }), -/* 638 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(610); +var use = __webpack_require__(614); var util = __webpack_require__(112); -var Cache = __webpack_require__(639); -var define = __webpack_require__(599); -var debug = __webpack_require__(612)('snapdragon:parser'); -var Position = __webpack_require__(640); -var utils = __webpack_require__(618); +var Cache = __webpack_require__(643); +var define = __webpack_require__(603); +var debug = __webpack_require__(616)('snapdragon:parser'); +var Position = __webpack_require__(644); +var utils = __webpack_require__(622); /** * Create a new `Parser` with the given `input` and `options`. @@ -76524,7 +76644,7 @@ module.exports = Parser; /***/ }), -/* 639 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76631,13 +76751,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 640 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(599); +var define = __webpack_require__(603); /** * Store position for a node @@ -76652,13 +76772,13 @@ module.exports = function Position(start, parser) { /***/ }), -/* 641 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(642); +var isExtendable = __webpack_require__(646); var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { @@ -76719,7 +76839,7 @@ function isEnum(obj, key) { /***/ }), -/* 642 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76740,14 +76860,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 643 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(644); -var extglob = __webpack_require__(659); +var nanomatch = __webpack_require__(648); +var extglob = __webpack_require__(663); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -76824,7 +76944,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 644 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76836,16 +76956,16 @@ function escapeExtglobs(compiler) { var util = __webpack_require__(112); var toRegex = __webpack_require__(528); -var extend = __webpack_require__(645); +var extend = __webpack_require__(649); /** * Local dependencies */ -var compilers = __webpack_require__(647); -var parsers = __webpack_require__(648); -var cache = __webpack_require__(651); -var utils = __webpack_require__(653); +var compilers = __webpack_require__(651); +var parsers = __webpack_require__(652); +var cache = __webpack_require__(655); +var utils = __webpack_require__(657); var MAX_LENGTH = 1024 * 64; /** @@ -77669,13 +77789,13 @@ module.exports = nanomatch; /***/ }), -/* 645 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(646); +var isExtendable = __webpack_require__(650); var assignSymbols = __webpack_require__(546); module.exports = Object.assign || function(obj/*, objects*/) { @@ -77736,7 +77856,7 @@ function isEnum(obj, key) { /***/ }), -/* 646 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77757,7 +77877,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 647 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78103,7 +78223,7 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 648 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78111,7 +78231,7 @@ module.exports = function(nanomatch, options) { var regexNot = __webpack_require__(547); var toRegex = __webpack_require__(528); -var isOdd = __webpack_require__(649); +var isOdd = __webpack_require__(653); /** * Characters to use in negation regex (we want to "not" match @@ -78497,7 +78617,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 649 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78510,7 +78630,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(650); +var isNumber = __webpack_require__(654); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -78524,7 +78644,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 650 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78552,14 +78672,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 651 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(652))(); +module.exports = new (__webpack_require__(656))(); /***/ }), -/* 652 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78572,7 +78692,7 @@ module.exports = new (__webpack_require__(652))(); -var MapCache = __webpack_require__(639); +var MapCache = __webpack_require__(643); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -78694,7 +78814,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 653 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78707,13 +78827,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(654)(); -var Snapdragon = __webpack_require__(571); -utils.define = __webpack_require__(655); -utils.diff = __webpack_require__(656); -utils.extend = __webpack_require__(645); -utils.pick = __webpack_require__(657); -utils.typeOf = __webpack_require__(658); +var isWindows = __webpack_require__(658)(); +var Snapdragon = __webpack_require__(573); +utils.define = __webpack_require__(659); +utils.diff = __webpack_require__(660); +utils.extend = __webpack_require__(649); +utils.pick = __webpack_require__(661); +utils.typeOf = __webpack_require__(662); utils.unique = __webpack_require__(550); /** @@ -79080,7 +79200,7 @@ utils.unixify = function(options) { /***/ }), -/* 654 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -79108,7 +79228,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 655 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79153,7 +79273,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 656 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79207,7 +79327,7 @@ function diffArray(one, two) { /***/ }), -/* 657 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79249,7 +79369,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 658 */ +/* 662 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -79384,7 +79504,7 @@ function isBuffer(val) { /***/ }), -/* 659 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79394,7 +79514,7 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(551); +var extend = __webpack_require__(587); var unique = __webpack_require__(550); var toRegex = __webpack_require__(528); @@ -79402,10 +79522,10 @@ var toRegex = __webpack_require__(528); * Local dependencies */ -var compilers = __webpack_require__(660); -var parsers = __webpack_require__(666); -var Extglob = __webpack_require__(669); -var utils = __webpack_require__(668); +var compilers = __webpack_require__(664); +var parsers = __webpack_require__(670); +var Extglob = __webpack_require__(673); +var utils = __webpack_require__(672); var MAX_LENGTH = 1024 * 64; /** @@ -79722,13 +79842,13 @@ module.exports = extglob; /***/ }), -/* 660 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(661); +var brackets = __webpack_require__(665); /** * Extglob compilers @@ -79898,7 +80018,7 @@ module.exports = function(extglob) { /***/ }), -/* 661 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79908,16 +80028,16 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(662); -var parsers = __webpack_require__(664); +var compilers = __webpack_require__(666); +var parsers = __webpack_require__(668); /** * Module dependencies */ -var debug = __webpack_require__(612)('expand-brackets'); -var extend = __webpack_require__(551); -var Snapdragon = __webpack_require__(571); +var debug = __webpack_require__(616)('expand-brackets'); +var extend = __webpack_require__(587); +var Snapdragon = __webpack_require__(573); var toRegex = __webpack_require__(528); /** @@ -80116,13 +80236,13 @@ module.exports = brackets; /***/ }), -/* 662 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(663); +var posix = __webpack_require__(667); module.exports = function(brackets) { brackets.compiler @@ -80210,7 +80330,7 @@ module.exports = function(brackets) { /***/ }), -/* 663 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80239,14 +80359,14 @@ module.exports = { /***/ }), -/* 664 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(665); -var define = __webpack_require__(599); +var utils = __webpack_require__(669); +var define = __webpack_require__(603); /** * Text regex @@ -80465,7 +80585,7 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 665 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80506,15 +80626,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 666 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(661); -var define = __webpack_require__(667); -var utils = __webpack_require__(668); +var brackets = __webpack_require__(665); +var define = __webpack_require__(671); +var utils = __webpack_require__(672); /** * Characters to use in text regex (we want to "not" match @@ -80669,7 +80789,7 @@ module.exports = parsers; /***/ }), -/* 667 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80707,14 +80827,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 668 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var regex = __webpack_require__(547); -var Cache = __webpack_require__(652); +var Cache = __webpack_require__(656); /** * Utils @@ -80783,7 +80903,7 @@ utils.createRegex = function(str) { /***/ }), -/* 669 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80793,16 +80913,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(571); -var define = __webpack_require__(667); -var extend = __webpack_require__(551); +var Snapdragon = __webpack_require__(573); +var define = __webpack_require__(671); +var extend = __webpack_require__(587); /** * Local dependencies */ -var compilers = __webpack_require__(660); -var parsers = __webpack_require__(666); +var compilers = __webpack_require__(664); +var parsers = __webpack_require__(670); /** * Customize Snapdragon parser and renderer @@ -80868,14 +80988,14 @@ module.exports = Extglob; /***/ }), -/* 670 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(659); -var nanomatch = __webpack_require__(644); +var extglob = __webpack_require__(663); +var nanomatch = __webpack_require__(648); var regexNot = __webpack_require__(547); var toRegex = __webpack_require__(528); var not; @@ -80958,14 +81078,14 @@ function textRegex(pattern) { /***/ }), -/* 671 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(652))(); +module.exports = new (__webpack_require__(656))(); /***/ }), -/* 672 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80978,12 +81098,12 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(571); -utils.define = __webpack_require__(673); -utils.diff = __webpack_require__(656); -utils.extend = __webpack_require__(641); -utils.pick = __webpack_require__(657); -utils.typeOf = __webpack_require__(674); +var Snapdragon = __webpack_require__(573); +utils.define = __webpack_require__(677); +utils.diff = __webpack_require__(660); +utils.extend = __webpack_require__(645); +utils.pick = __webpack_require__(661); +utils.typeOf = __webpack_require__(678); utils.unique = __webpack_require__(550); /** @@ -81281,7 +81401,7 @@ utils.unixify = function(options) { /***/ }), -/* 673 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81326,7 +81446,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 674 */ +/* 678 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -81461,7 +81581,7 @@ function isBuffer(val) { /***/ }), -/* 675 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81480,9 +81600,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(676); -var reader_1 = __webpack_require__(689); -var fs_stream_1 = __webpack_require__(693); +var readdir = __webpack_require__(680); +var reader_1 = __webpack_require__(693); +var fs_stream_1 = __webpack_require__(697); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81543,15 +81663,15 @@ exports.default = ReaderAsync; /***/ }), -/* 676 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(677); -const readdirAsync = __webpack_require__(685); -const readdirStream = __webpack_require__(688); +const readdirSync = __webpack_require__(681); +const readdirAsync = __webpack_require__(689); +const readdirStream = __webpack_require__(692); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81635,7 +81755,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 677 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81643,11 +81763,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(678); +const DirectoryReader = __webpack_require__(682); let syncFacade = { - fs: __webpack_require__(683), - forEach: __webpack_require__(684), + fs: __webpack_require__(687), + forEach: __webpack_require__(688), sync: true }; @@ -81676,7 +81796,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 678 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81685,9 +81805,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(138).Readable; const EventEmitter = __webpack_require__(156).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(679); -const stat = __webpack_require__(681); -const call = __webpack_require__(682); +const normalizeOptions = __webpack_require__(683); +const stat = __webpack_require__(685); +const call = __webpack_require__(686); /** * Asynchronously reads the contents of a directory and streams the results @@ -82063,14 +82183,14 @@ module.exports = DirectoryReader; /***/ }), -/* 679 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(680); +const globToRegExp = __webpack_require__(684); module.exports = normalizeOptions; @@ -82247,7 +82367,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 680 */ +/* 684 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82384,13 +82504,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 681 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(682); +const call = __webpack_require__(686); module.exports = stat; @@ -82465,7 +82585,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 682 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82526,14 +82646,14 @@ function callOnce (fn) { /***/ }), -/* 683 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(682); +const call = __webpack_require__(686); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82597,7 +82717,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 684 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82626,7 +82746,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 685 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82634,12 +82754,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(686); -const DirectoryReader = __webpack_require__(678); +const maybe = __webpack_require__(690); +const DirectoryReader = __webpack_require__(682); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(687), + forEach: __webpack_require__(691), async: true }; @@ -82681,7 +82801,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 686 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82708,7 +82828,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 687 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82744,7 +82864,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 688 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82752,11 +82872,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(678); +const DirectoryReader = __webpack_require__(682); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(687), + forEach: __webpack_require__(691), async: true }; @@ -82776,16 +82896,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 689 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(690); -var entry_1 = __webpack_require__(692); -var pathUtil = __webpack_require__(691); +var deep_1 = __webpack_require__(694); +var entry_1 = __webpack_require__(696); +var pathUtil = __webpack_require__(695); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -82851,13 +82971,13 @@ exports.default = Reader; /***/ }), -/* 690 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(691); +var pathUtils = __webpack_require__(695); var patternUtils = __webpack_require__(522); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { @@ -82941,7 +83061,7 @@ exports.default = DeepFilter; /***/ }), -/* 691 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82972,13 +83092,13 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 692 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(691); +var pathUtils = __webpack_require__(695); var patternUtils = __webpack_require__(522); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { @@ -83064,7 +83184,7 @@ exports.default = EntryFilter; /***/ }), -/* 693 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83084,8 +83204,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(694); -var fs_1 = __webpack_require__(698); +var fsStat = __webpack_require__(698); +var fs_1 = __webpack_require__(702); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -83135,14 +83255,14 @@ exports.default = FileSystemStream; /***/ }), -/* 694 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(695); -const statProvider = __webpack_require__(697); +const optionsManager = __webpack_require__(699); +const statProvider = __webpack_require__(701); /** * Asynchronous API. */ @@ -83173,13 +83293,13 @@ exports.statSync = statSync; /***/ }), -/* 695 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(696); +const fsAdapter = __webpack_require__(700); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -83192,7 +83312,7 @@ exports.prepare = prepare; /***/ }), -/* 696 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83215,7 +83335,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 697 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83267,7 +83387,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 698 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83298,7 +83418,7 @@ exports.default = FileSystem; /***/ }), -/* 699 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83318,9 +83438,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(676); -var reader_1 = __webpack_require__(689); -var fs_stream_1 = __webpack_require__(693); +var readdir = __webpack_require__(680); +var reader_1 = __webpack_require__(693); +var fs_stream_1 = __webpack_require__(697); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83388,7 +83508,7 @@ exports.default = ReaderStream; /***/ }), -/* 700 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83407,9 +83527,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(676); -var reader_1 = __webpack_require__(689); -var fs_sync_1 = __webpack_require__(701); +var readdir = __webpack_require__(680); +var reader_1 = __webpack_require__(693); +var fs_sync_1 = __webpack_require__(705); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83469,7 +83589,7 @@ exports.default = ReaderSync; /***/ }), -/* 701 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83488,8 +83608,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(694); -var fs_1 = __webpack_require__(698); +var fsStat = __webpack_require__(698); +var fs_1 = __webpack_require__(702); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83535,7 +83655,7 @@ exports.default = FileSystemSync; /***/ }), -/* 702 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83551,7 +83671,7 @@ exports.flatten = flatten; /***/ }), -/* 703 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83572,13 +83692,13 @@ exports.merge = merge; /***/ }), -/* 704 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(705); +const pathType = __webpack_require__(709); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83644,13 +83764,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 705 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(706); +const pify = __webpack_require__(710); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83693,7 +83813,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 706 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83784,7 +83904,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 707 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83792,9 +83912,9 @@ module.exports = (obj, opts) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const fastGlob = __webpack_require__(518); -const gitIgnore = __webpack_require__(708); -const pify = __webpack_require__(709); -const slash = __webpack_require__(710); +const gitIgnore = __webpack_require__(712); +const pify = __webpack_require__(713); +const slash = __webpack_require__(714); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -83892,7 +84012,7 @@ module.exports.sync = options => { /***/ }), -/* 708 */ +/* 712 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -84361,7 +84481,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 709 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84436,7 +84556,7 @@ module.exports = (input, options) => { /***/ }), -/* 710 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84454,7 +84574,7 @@ module.exports = input => { /***/ }), -/* 711 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84467,7 +84587,7 @@ module.exports = input => { -var isGlob = __webpack_require__(712); +var isGlob = __webpack_require__(716); module.exports = function hasGlob(val) { if (val == null) return false; @@ -84487,7 +84607,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 712 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84518,17 +84638,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 713 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(714); -const CpFileError = __webpack_require__(717); -const fs = __webpack_require__(719); -const ProgressEmitter = __webpack_require__(722); +const pEvent = __webpack_require__(718); +const CpFileError = __webpack_require__(721); +const fs = __webpack_require__(723); +const ProgressEmitter = __webpack_require__(726); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84642,12 +84762,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 714 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(715); +const pTimeout = __webpack_require__(719); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -84938,12 +85058,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 715 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(716); +const pFinally = __webpack_require__(720); class TimeoutError extends Error { constructor(message) { @@ -84989,7 +85109,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 716 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85011,12 +85131,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 717 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(718); +const NestedError = __webpack_require__(722); class CpFileError extends NestedError { constructor(message, nested) { @@ -85030,7 +85150,7 @@ module.exports = CpFileError; /***/ }), -/* 718 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -85086,16 +85206,16 @@ module.exports = NestedError; /***/ }), -/* 719 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(720); -const pEvent = __webpack_require__(714); -const CpFileError = __webpack_require__(717); +const makeDir = __webpack_require__(724); +const pEvent = __webpack_require__(718); +const CpFileError = __webpack_require__(721); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -85192,7 +85312,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 720 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85200,7 +85320,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const {promisify} = __webpack_require__(112); -const semver = __webpack_require__(721); +const semver = __webpack_require__(725); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -85355,7 +85475,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 721 */ +/* 725 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -86957,7 +87077,7 @@ function coerce (version, options) { /***/ }), -/* 722 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86998,7 +87118,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 723 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87044,12 +87164,12 @@ exports.default = module.exports; /***/ }), -/* 724 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(725); +const pMap = __webpack_require__(729); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -87066,7 +87186,7 @@ module.exports.default = pFilter; /***/ }), -/* 725 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87145,12 +87265,12 @@ module.exports.default = pMap; /***/ }), -/* 726 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(718); +const NestedError = __webpack_require__(722); class CpyError extends NestedError { constructor(message, nested) { diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index f1eefab0f1fd0..b8947d1b3b6d0 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -7,7 +7,6 @@ "lib": "lib" }, "scripts": { - "test": "../../node_modules/.bin/jest", "format": "../../node_modules/.bin/prettier **/*.js --write" }, "author": "", diff --git a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts index 606902228e1b7..e5bad88e5e7bf 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/docker_servers/docker_servers_service.ts @@ -124,7 +124,11 @@ export class DockerServersService { lifecycle.cleanup.add(() => { try { execa.sync('docker', ['kill', containerId]); - execa.sync('docker', ['rm', containerId]); + // we don't remove the containers on CI because removing them causes the + // network list to be updated and aborts all in-flight requests in Chrome + if (!process.env.CI) { + execa.sync('docker', ['rm', containerId]); + } } catch (error) { if ( error.message.includes(`Container ${containerId} is not running`) || diff --git a/src/core/public/http/external_url_service.test.ts b/src/core/public/http/external_url_service.test.ts new file mode 100644 index 0000000000000..af34dba5e6216 --- /dev/null +++ b/src/core/public/http/external_url_service.test.ts @@ -0,0 +1,494 @@ +/* + * 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 { ExternalUrlConfig } from 'src/core/server/types'; + +import { injectedMetadataServiceMock } from '../mocks'; +import { Sha256 } from '../utils'; + +import { ExternalUrlService } from './external_url_service'; + +const setupService = ({ + location, + serverBasePath, + policy, +}: { + location: URL; + serverBasePath: string; + policy: ExternalUrlConfig['policy']; +}) => { + const hashedPolicies = policy.map((entry) => { + // If the host contains a `[`, then it's likely an IPv6 address. Otherwise, append a `.` if it doesn't already contain one + const hostToHash = + entry.host && !entry.host.includes('[') && !entry.host.endsWith('.') + ? `${entry.host}.` + : entry.host; + return { + ...entry, + host: hostToHash ? new Sha256().update(hostToHash, 'utf8').digest('hex') : undefined, + }; + }); + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + injectedMetadata.getExternalUrlConfig.mockReturnValue({ policy: hashedPolicies }); + injectedMetadata.getServerBasePath.mockReturnValue(serverBasePath); + + const service = new ExternalUrlService(); + return { + setup: service.setup({ + injectedMetadata, + location, + }), + }; +}; + +const internalRequestScenarios = [ + { + description: 'without any policies', + allowExternal: false, + policy: [], + }, + { + description: 'with an unrestricted policy', + allowExternal: true, + policy: [ + { + allow: true, + }, + ], + }, + { + description: 'with a fully restricted policy', + allowExternal: false, + policy: [ + { + allow: false, + }, + ], + }, +]; + +describe('External Url Service', () => { + describe('#validateUrl', () => { + describe('internal requests with a server base path', () => { + const serverBasePath = '/base-path'; + const serverRoot = `https://my-kibana.example.com:5601`; + const kibanaRoot = `${serverRoot}${serverBasePath}`; + const location = new URL(`${kibanaRoot}/app/management?q=1&bar=false#some-hash`); + + internalRequestScenarios.forEach(({ description, policy, allowExternal }) => { + describe(description, () => { + it('allows relative URLs that start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`${serverBasePath}${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${kibanaRoot}${urlCandidate}`); + }); + + it('allows absolute URLs to Kibana that start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `${kibanaRoot}/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${kibanaRoot}/some/path?foo=bar`); + }); + + if (allowExternal) { + it('allows absolute URLs to Kibana that do not start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `${serverRoot}/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${serverRoot}/some/path?foo=bar`); + }); + + it('allows relative URLs that attempt to bypass the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/../../path?foo=bar`; + const result = setup.validateUrl(`${serverBasePath}${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${serverRoot}/path?foo=bar`); + }); + + it('allows relative URLs that do not start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${serverRoot}/some/path?foo=bar`); + }); + } else { + it('disallows absolute URLs to Kibana that do not start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `${serverRoot}/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + + it('disallows relative URLs that attempt to bypass the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/../../path?foo=bar`; + const result = setup.validateUrl(`${serverBasePath}${urlCandidate}`); + + expect(result).toBeNull(); + }); + + it('disallows relative URLs that do not start with the server base path', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + } + }); + }); + + describe('handles protocol resolution bypass', () => { + it('does not allow relative URLs that include a host', () => { + const { setup } = setupService({ location, serverBasePath, policy: [] }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`//www.google.com${serverBasePath}${urlCandidate}`); + + expect(result).toBeNull(); + }); + + it('does allow relative URLs that include a host if allowed by policy', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + }, + ], + }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`//www.google.com${serverBasePath}${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual( + `https://www.google.com${serverBasePath}${urlCandidate}` + ); + }); + }); + }); + + describe('internal requests without a server base path', () => { + const serverBasePath = ''; + const serverRoot = `https://my-kibana.example.com:5601`; + const kibanaRoot = `${serverRoot}${serverBasePath}`; + const location = new URL(`${kibanaRoot}/app/management?q=1&bar=false#some-hash`); + + internalRequestScenarios.forEach(({ description, policy }) => { + describe(description, () => { + it('allows relative URLs', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`${serverBasePath}${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${kibanaRoot}${urlCandidate}`); + }); + + it('allows absolute URLs to Kibana', () => { + const { setup } = setupService({ location, serverBasePath, policy }); + const urlCandidate = `${kibanaRoot}/some/path?foo=bar`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`${kibanaRoot}/some/path?foo=bar`); + }); + }); + }); + + describe('handles protocol resolution bypass', () => { + it('does not allow relative URLs that include a host', () => { + const { setup } = setupService({ location, serverBasePath, policy: [] }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`//www.google.com${urlCandidate}`); + + expect(result).toBeNull(); + }); + + it('allows relative URLs that include a host in the allow list', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + }, + ], + }); + const urlCandidate = `/some/path?foo=bar`; + const result = setup.validateUrl(`//www.google.com${urlCandidate}`); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(`https://www.google.com${urlCandidate}`); + }); + }); + }); + + describe('external requests', () => { + const serverBasePath = '/base-path'; + const serverRoot = `https://my-kibana.example.com:5601`; + const kibanaRoot = `${serverRoot}${serverBasePath}`; + const location = new URL(`${kibanaRoot}/app/management?q=1&bar=false#some-hash`); + + it('does not allow external urls by default', () => { + const { setup } = setupService({ location, serverBasePath, policy: [] }); + const urlCandidate = `http://www.google.com`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + + it('does not allow external urls with a fully restricted policy', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: false, + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + + it('allows external urls with an unrestricted policy', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with a matching host and protocol in the allow list', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with a partially matching host and protocol in the allow list', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'google.com', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with a partially matching host and protocol in the allow list when the URL includes the root domain', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'google.com', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://www.google.com./foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with an IPv4 address', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: '192.168.10.12', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://192.168.10.12/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with an IPv6 address', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls that specify a locally addressable host', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'some-host-name', + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://some-host-name/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('disallows external urls with a matching host and unmatched protocol', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + protocol: 'https', + }, + ], + }); + const urlCandidate = `http://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + + it('allows external urls with a matching host and any protocol', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + host: 'www.google.com', + }, + ], + }); + const urlCandidate = `ftp://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('allows external urls with any host and matching protocol', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + protocol: 'https', + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeInstanceOf(URL); + expect(result?.toString()).toEqual(urlCandidate); + }); + + it('disallows external urls that match multiple rules, one of which denies the request', () => { + const { setup } = setupService({ + location, + serverBasePath, + policy: [ + { + allow: true, + protocol: 'https', + }, + { + allow: false, + host: 'www.google.com', + }, + ], + }); + const urlCandidate = `https://www.google.com/foo?bar=baz`; + const result = setup.validateUrl(urlCandidate); + + expect(result).toBeNull(); + }); + }); + }); +}); diff --git a/src/core/public/http/external_url_service.ts b/src/core/public/http/external_url_service.ts new file mode 100644 index 0000000000000..e975451a7fdaa --- /dev/null +++ b/src/core/public/http/external_url_service.ts @@ -0,0 +1,111 @@ +/* + * 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 { IExternalUrlPolicy } from 'src/core/server/types'; + +import { CoreService } from 'src/core/types'; +import { IExternalUrl } from './types'; +import { InjectedMetadataSetup } from '../injected_metadata'; +import { Sha256 } from '../utils'; + +interface SetupDeps { + location: Pick; + injectedMetadata: InjectedMetadataSetup; +} + +function* getHostHashes(actualHost: string) { + yield new Sha256().update(actualHost, 'utf8').digest('hex'); + let host = actualHost.substr(actualHost.indexOf('.') + 1); + while (host) { + yield new Sha256().update(host, 'utf8').digest('hex'); + if (host.indexOf('.') === -1) { + break; + } + host = host.substr(host.indexOf('.') + 1); + } +} + +const isHostMatch = (actualHost: string, ruleHostHash: string) => { + // If the host contains a `[`, then it's likely an IPv6 address. Otherwise, append a `.` if it doesn't already contain one + const hostToHash = + !actualHost.includes('[') && !actualHost.endsWith('.') ? `${actualHost}.` : actualHost; + for (const hash of getHostHashes(hostToHash)) { + if (hash === ruleHostHash) { + return true; + } + } + return false; +}; + +const isProtocolMatch = (actualProtocol: string, ruleProtocol: string) => { + return normalizeProtocol(actualProtocol) === normalizeProtocol(ruleProtocol); +}; + +function normalizeProtocol(protocol: string) { + return protocol.endsWith(':') ? protocol.slice(0, -1).toLowerCase() : protocol.toLowerCase(); +} + +const createExternalUrlValidation = ( + rules: IExternalUrlPolicy[], + location: Pick, + serverBasePath: string +) => { + const base = new URL(location.origin + serverBasePath); + return function validateExternalUrl(next: string) { + const url = new URL(next, base); + + const isInternalURL = + url.origin === base.origin && + (!serverBasePath || url.pathname.startsWith(`${serverBasePath}/`)); + + if (isInternalURL) { + return url; + } + + let allowed: null | boolean = null; + rules.forEach((rule) => { + const hostMatch = rule.host ? isHostMatch(url.hostname || '', rule.host) : true; + + const protocolMatch = rule.protocol ? isProtocolMatch(url.protocol, rule.protocol) : true; + + const isRuleMatch = hostMatch && protocolMatch; + + if (isRuleMatch && allowed !== false) { + allowed = rule.allow; + } + }); + + return allowed === true ? url : null; + }; +}; + +export class ExternalUrlService implements CoreService { + setup({ injectedMetadata, location }: SetupDeps): IExternalUrl { + const serverBasePath = injectedMetadata.getServerBasePath(); + const { policy } = injectedMetadata.getExternalUrlConfig(); + + return { + validateUrl: createExternalUrlValidation(policy, location, serverBasePath), + }; + } + + start() {} + + stop() {} +} diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 68533159765fb..025336487c855 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -41,6 +41,9 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ register: jest.fn(), isAnonymous: jest.fn(), }, + externalUrl: { + validateUrl: jest.fn(), + }, addLoadingCountSource: jest.fn(), getLoadingCount$: jest.fn().mockReturnValue(new BehaviorSubject(0)), intercept: jest.fn(), diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 2eaaefe285755..a65eb5f76e1ac 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -25,6 +25,7 @@ import { AnonymousPathsService } from './anonymous_paths_service'; import { LoadingCountService } from './loading_count_service'; import { Fetch } from './fetch'; import { CoreService } from '../../types'; +import { ExternalUrlService } from './external_url_service'; interface HttpDeps { injectedMetadata: InjectedMetadataSetup; @@ -51,6 +52,7 @@ export class HttpService implements CoreService { this.service = { basePath, anonymousPaths: this.anonymousPaths.setup({ basePath }), + externalUrl: new ExternalUrlService().setup({ injectedMetadata, location: window.location }), intercept: fetchService.intercept.bind(fetchService), fetch: fetchService.fetch.bind(fetchService), delete: fetchService.delete.bind(fetchService), diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 7285d1a4288dc..5910aa0fc3238 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -33,6 +33,8 @@ export interface HttpSetup { */ anonymousPaths: IAnonymousPaths; + externalUrl: IExternalUrl; + /** * Adds a new {@link HttpInterceptor} to the global HTTP client. * @param interceptor a {@link HttpInterceptor} @@ -112,6 +114,23 @@ export interface IBasePath { */ readonly publicBaseUrl?: string; } +/** + * APIs for working with external URLs. + * + * @public + */ +export interface IExternalUrl { + /** + * Determines if the provided URL is a valid location to send users. + * Validation is based on the configured allow list in kibana.yml. + * + * If the URL is valid, then a URL will be returned. + * Otherwise, this will return null. + * + * @param relativeOrAbsoluteUrl + */ + validateUrl(relativeOrAbsoluteUrl: string): URL | null; +} /** * APIs for denoting paths as not requiring authentication diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 557529fc94dc4..8e240bfe91d48 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -77,7 +77,7 @@ import { HandlerParameters, } from './context'; -export { PackageInfo, EnvironmentMode } from '../server/types'; +export { PackageInfo, EnvironmentMode, IExternalUrlPolicy } from '../server/types'; export { CoreContext, CoreSystem } from './core_system'; export { DEFAULT_APP_CATEGORIES } from '../utils'; export { @@ -164,6 +164,7 @@ export { HttpHandler, IBasePath, IAnonymousPaths, + IExternalUrl, IHttpInterceptController, IHttpFetchError, IHttpResponseInterceptorOverrides, diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 96282caa62c0a..ec05edcbbf25c 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -27,6 +27,7 @@ const createSetupContractMock = () => { getKibanaVersion: jest.fn(), getKibanaBranch: jest.fn(), getCspConfig: jest.fn(), + getExternalUrlConfig: jest.fn(), getAnonymousStatusPage: jest.fn(), getLegacyMetadata: jest.fn(), getPlugins: jest.fn(), @@ -35,6 +36,7 @@ const createSetupContractMock = () => { getKibanaBuildNumber: jest.fn(), }; setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); + setupContract.getExternalUrlConfig.mockReturnValue({ policy: [] }); setupContract.getKibanaVersion.mockReturnValue('kibanaVersion'); setupContract.getAnonymousStatusPage.mockReturnValue(false); setupContract.getLegacyMetadata.mockReturnValue({ diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 283710980e3ce..51025e24140da 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -22,6 +22,7 @@ import { deepFreeze } from '@kbn/std'; import { DiscoveredPlugin, PluginName } from '../../server'; import { EnvironmentMode, + IExternalUrlPolicy, PackageInfo, UiSettingsParams, UserProvidedValues, @@ -49,6 +50,9 @@ export interface InjectedMetadataParams { csp: { warnLegacyBrowsers: boolean; }; + externalUrl: { + policy: IExternalUrlPolicy[]; + }; vars: { [key: string]: unknown; }; @@ -112,6 +116,10 @@ export class InjectedMetadataService { return this.state.csp; }, + getExternalUrlConfig: () => { + return this.state.externalUrl; + }, + getPlugins: () => { return this.state.uiPlugins; }, @@ -154,6 +162,9 @@ export interface InjectedMetadataSetup { getCspConfig: () => { warnLegacyBrowsers: boolean; }; + getExternalUrlConfig: () => { + policy: IExternalUrlPolicy[]; + }; /** * An array of frontend plugins in topological order. */ diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 0b1d3f8263a23..65912e0954261 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -727,6 +727,8 @@ export interface HttpSetup { anonymousPaths: IAnonymousPaths; basePath: IBasePath; delete: HttpHandler; + // (undocumented) + externalUrl: IExternalUrl; fetch: HttpHandler; get: HttpHandler; getLoadingCount$(): Observable; @@ -777,6 +779,18 @@ export interface IContextContainer> { // @public export type IContextProvider, TContextName extends keyof HandlerContextType> = (context: PartialExceptFor, 'core'>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; +// @public +export interface IExternalUrl { + validateUrl(relativeOrAbsoluteUrl: string): URL | null; +} + +// @public +export interface IExternalUrlPolicy { + allow: boolean; + host?: string; + protocol?: string; +} + // @public (undocumented) export interface IHttpFetchError extends Error { // (undocumented) diff --git a/src/core/server/external_url/config.test.ts b/src/core/server/external_url/config.test.ts new file mode 100644 index 0000000000000..eeaf3751904d4 --- /dev/null +++ b/src/core/server/external_url/config.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { config } from './config'; + +describe('externalUrl config', () => { + it('provides a default policy allowing all external urls', () => { + expect(config.schema.validate({})).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + } + `); + }); + + it('allows an empty policy', () => { + expect(config.schema.validate({ policy: [] })).toMatchInlineSnapshot(` + Object { + "policy": Array [], + } + `); + }); + + it('allows a policy with just a protocol', () => { + expect( + config.schema.validate({ + policy: [ + { + allow: true, + protocol: 'http', + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + "protocol": "http", + }, + ], + } + `); + }); + + it('allows a policy with just a host', () => { + expect( + config.schema.validate({ + policy: [ + { + allow: true, + host: 'www.google.com', + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + "host": "www.google.com", + }, + ], + } + `); + }); + + it('allows a policy with both host and protocol', () => { + expect( + config.schema.validate({ + policy: [ + { + allow: true, + protocol: 'http', + host: 'www.google.com', + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + "host": "www.google.com", + "protocol": "http", + }, + ], + } + `); + }); + + it('allows a policy without a host or protocol', () => { + expect( + config.schema.validate({ + policy: [ + { + allow: true, + }, + ], + }) + ).toMatchInlineSnapshot(` + Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + } + `); + }); + + describe('protocols', () => { + ['http', 'https', 'ftp', 'ftps', 'custom-protocol+123.bar'].forEach((protocol) => { + it(`allows a protocol of "${protocol}"`, () => { + config.schema.validate({ + policy: [ + { + allow: true, + protocol, + }, + ], + }); + }); + }); + + ['1http', '', 'custom-protocol()', 'https://'].forEach((protocol) => { + it(`disallows a protocol of "${protocol}"`, () => { + expect(() => + config.schema.validate({ + policy: [ + { + allow: true, + protocol, + }, + ], + }) + ).toThrowError(); + }); + }); + }); +}); diff --git a/src/core/server/external_url/config.ts b/src/core/server/external_url/config.ts new file mode 100644 index 0000000000000..4a26365a0c93d --- /dev/null +++ b/src/core/server/external_url/config.ts @@ -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 { TypeOf, schema } from '@kbn/config-schema'; +import { IExternalUrlPolicy } from '.'; + +/** + * @internal + */ +export type ExternalUrlConfigType = TypeOf; + +const allowSchema = schema.boolean(); + +const hostSchema = schema.string(); + +const protocolSchema = schema.string({ + validate: (value) => { + // tools.ietf.org/html/rfc3986#section-3.1 + // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + const schemaRegex = /^[a-zA-Z][a-zA-Z0-9\+\-\.]*$/; + if (!schemaRegex.test(value)) + throw new Error( + 'Protocol must begin with a letter, and can only contain letters, numbers, and the following characters: `+ - .`' + ); + }, +}); + +const policySchema = schema.object({ + allow: allowSchema, + protocol: schema.maybe(protocolSchema), + host: schema.maybe(hostSchema), +}); + +export const config = { + path: 'externalUrl', + schema: schema.object({ + policy: schema.arrayOf(policySchema, { + defaultValue: [ + { + allow: true, + }, + ], + }), + }), +}; diff --git a/src/core/server/external_url/external_url_config.ts b/src/core/server/external_url/external_url_config.ts new file mode 100644 index 0000000000000..065a9cd1d2609 --- /dev/null +++ b/src/core/server/external_url/external_url_config.ts @@ -0,0 +1,101 @@ +/* + * 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 { createSHA256Hash } from '../utils'; +import { config } from './config'; + +const DEFAULT_CONFIG = Object.freeze(config.schema.validate({})); + +/** + * External Url configuration for use in Kibana. + * @public + */ +export interface IExternalUrlConfig { + /** + * A set of policies describing which external urls are allowed. + */ + readonly policy: IExternalUrlPolicy[]; +} + +/** + * A policy describing whether access to an external destination is allowed. + * @public + */ +export interface IExternalUrlPolicy { + /** + * Indicates if this policy allows or denies access to the described destination. + */ + allow: boolean; + + /** + * Optional host describing the external destination. + * May be combined with `protocol`. + * + * @example + * ```ts + * // allows access to all of google.com, using any protocol. + * allow: true, + * host: 'google.com' + * ``` + */ + host?: string; + + /** + * Optional protocol describing the external destination. + * May be combined with `host`. + * + * @example + * ```ts + * // allows access to all destinations over the `https` protocol. + * allow: true, + * protocol: 'https' + * ``` + */ + protocol?: string; +} + +/** + * External Url configuration for use in Kibana. + * @public + */ +export class ExternalUrlConfig implements IExternalUrlConfig { + static readonly DEFAULT = new ExternalUrlConfig(DEFAULT_CONFIG); + + public readonly policy: IExternalUrlPolicy[]; + /** + * Returns the default External Url configuration when passed with no config + * @internal + */ + constructor(rawConfig: IExternalUrlConfig) { + this.policy = rawConfig.policy.map((entry) => { + if (entry.host) { + // If the host contains a `[`, then it's likely an IPv6 address. Otherwise, append a `.` if it doesn't already contain one + const hostToHash = + entry.host && !entry.host.includes('[') && !entry.host.endsWith('.') + ? `${entry.host}.` + : entry.host; + return { + ...entry, + host: createSHA256Hash(hostToHash), + }; + } + return entry; + }); + } +} diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts b/src/core/server/external_url/index.ts similarity index 76% rename from src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts rename to src/core/server/external_url/index.ts index b318587057c76..dfc8e753fa644 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts +++ b/src/core/server/external_url/index.ts @@ -17,10 +17,5 @@ * under the License. */ -export declare const MINUTE: string; -export declare const HOUR: string; -export declare const DAY: string; -export declare const WEEK: string; -export declare const MONTH: string; -export declare const YEAR: string; -export declare const CronEditor: any; +export { ExternalUrlConfig, IExternalUrlConfig, IExternalUrlPolicy } from './external_url_config'; +export { ExternalUrlConfigType, config } from './config'; diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index 7ac7e4b9712d0..0e7b55b7d35ab 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -46,29 +46,40 @@ const setupDeps = { context: contextSetup, }; -configService.atPath.mockReturnValue( - new BehaviorSubject({ - hosts: ['http://1.2.3.4'], - maxPayload: new ByteSizeValue(1024), - autoListen: true, - healthCheck: { - delay: 2000, - }, - ssl: { - verificationMode: 'none', - }, - compression: { enabled: true }, - xsrf: { - disableProtection: true, - allowlist: [], - }, - customResponseHeaders: {}, - requestId: { - allowFromAnyIp: true, - ipAllowlist: [], - }, - } as any) -); +configService.atPath.mockImplementation((path) => { + if (path === 'server') { + return new BehaviorSubject({ + hosts: ['http://1.2.3.4'], + maxPayload: new ByteSizeValue(1024), + autoListen: true, + healthCheck: { + delay: 2000, + }, + ssl: { + verificationMode: 'none', + }, + compression: { enabled: true }, + xsrf: { + disableProtection: true, + allowlist: [], + }, + customResponseHeaders: {}, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + } as any); + } + if (path === 'externalUrl') { + return new BehaviorSubject({ + policy: [], + } as any); + } + if (path === 'csp') { + return new BehaviorSubject({} as any); + } + throw new Error(`Unexpected config path: ${path}`); +}); beforeEach(() => { logger = loggingSystemMock.create(); diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 6538c1ae973b7..c82e7c3796e4b 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -20,6 +20,7 @@ import uuid from 'uuid'; import { config, HttpConfig } from './http_config'; import { CspConfig } from '../csp'; +import { ExternalUrlConfig } from '../external_url'; const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost']; const invalidHostname = 'asdf$%^'; @@ -344,7 +345,7 @@ describe('HttpConfig', () => { }, }, }); - const httpConfig = new HttpConfig(rawConfig, CspConfig.DEFAULT); + const httpConfig = new HttpConfig(rawConfig, CspConfig.DEFAULT, ExternalUrlConfig.DEFAULT); expect(httpConfig.customResponseHeaders).toEqual({ string: 'string', diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 9a425fa645503..d26f077723ce3 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -22,6 +22,7 @@ import { hostname } from 'os'; import url from 'url'; import { CspConfigType, CspConfig, ICspConfig } from '../csp'; +import { ExternalUrlConfig, IExternalUrlConfig } from '../external_url'; import { SslConfig, sslSchema } from './ssl_config'; const validBasePathRegex = /^\/.*[^\/]$/; @@ -156,13 +157,18 @@ export class HttpConfig { public ssl: SslConfig; public compression: { enabled: boolean; referrerWhitelist?: string[] }; public csp: ICspConfig; + public externalUrl: IExternalUrlConfig; public xsrf: { disableProtection: boolean; allowlist: string[] }; public requestId: { allowFromAnyIp: boolean; ipAllowlist: string[] }; /** * @internal */ - constructor(rawHttpConfig: HttpConfigType, rawCspConfig: CspConfigType) { + constructor( + rawHttpConfig: HttpConfigType, + rawCspConfig: CspConfigType, + rawExternalUrlConfig: ExternalUrlConfig + ) { this.autoListen = rawHttpConfig.autoListen; this.host = rawHttpConfig.host; this.port = rawHttpConfig.port; @@ -186,6 +192,7 @@ export class HttpConfig { this.ssl = new SslConfig(rawHttpConfig.ssl || {}); this.compression = rawHttpConfig.compression; this.csp = new CspConfig(rawCspConfig); + this.externalUrl = rawExternalUrlConfig; this.xsrf = rawHttpConfig.xsrf; this.requestId = rawHttpConfig.requestId; } diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 552f41d912417..d19bee27dd4cf 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -37,6 +37,7 @@ import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; import { OnPreAuthToolkit } from './lifecycle/on_pre_auth'; import { OnPreResponseToolkit } from './lifecycle/on_pre_response'; import { configMock } from '../config/mocks'; +import { ExternalUrlConfig } from '../external_url'; type BasePathMocked = jest.Mocked; type AuthMocked = jest.Mocked; @@ -105,6 +106,7 @@ const createInternalSetupContractMock = () => { registerStaticDir: jest.fn(), basePath: createBasePathMock(), csp: CspConfig.DEFAULT, + externalUrl: ExternalUrlConfig.DEFAULT, auth: createAuthMock(), getAuthHeaders: jest.fn(), getServerInfo: jest.fn(), diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index 3d55322461288..9075cb293667a 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -30,6 +30,7 @@ import { ConfigService, Env } from '../config'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { contextServiceMock } from '../context/context_service.mock'; import { config as cspConfig } from '../csp'; +import { config as externalUrlConfig } from '../external_url'; const logger = loggingSystemMock.create(); const env = Env.createDefault(REPO_ROOT, getEnvOptions()); @@ -48,6 +49,7 @@ const createConfigService = (value: Partial = {}) => { ); configService.setSchema(config.path, config.schema); configService.setSchema(cspConfig.path, cspConfig.schema); + configService.setSchema(externalUrlConfig.path, externalUrlConfig.schema); return configService; }; const contextSetup = contextServiceMock.createSetupContract(); diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 171a20160d26d..ae2e82d8b2241 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -44,6 +44,11 @@ import { import { RequestHandlerContext } from '../../server'; import { registerCoreHandlers } from './lifecycle_handlers'; +import { + ExternalUrlConfigType, + config as externalUrlConfig, + ExternalUrlConfig, +} from '../external_url'; interface SetupDeps { context: ContextSetup; @@ -73,7 +78,8 @@ export class HttpService this.config$ = combineLatest([ configService.atPath(httpConfig.path), configService.atPath(cspConfig.path), - ]).pipe(map(([http, csp]) => new HttpConfig(http, csp))); + configService.atPath(externalUrlConfig.path), + ]).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl))); this.httpServer = new HttpServer(logger, 'Kibana'); this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server')); } @@ -103,6 +109,8 @@ export class HttpService this.internalSetup = { ...serverContract, + externalUrl: new ExternalUrlConfig(config.externalUrl), + createRouter: (path: string, pluginId: PluginOpaqueId = this.coreContext.coreId) => { const enhanceHandler = this.requestHandlerContext!.createHandler.bind(null, pluginId); const router = new Router(path, this.log, enhanceHandler); diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts index 1423e27b914a3..a409a7485a0ef 100644 --- a/src/core/server/http/http_tools.test.ts +++ b/src/core/server/http/http_tools.test.ts @@ -136,6 +136,7 @@ describe('getServerOptions', () => { certificate: 'some-certificate-path', }, }), + {} as any, {} as any ); @@ -165,6 +166,7 @@ describe('getServerOptions', () => { clientAuthentication: 'required', }, }), + {} as any, {} as any ); diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index 7df35b04c66cf..ba7f55caeba22 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -50,26 +50,37 @@ describe('core lifecycle handlers', () => { beforeEach(async () => { const configService = configServiceMock.create(); - configService.atPath.mockReturnValue( - new BehaviorSubject({ - hosts: ['localhost'], - maxPayload: new ByteSizeValue(1024), - autoListen: true, - ssl: { - enabled: false, - }, - compression: { enabled: true }, - name: kibanaName, - customResponseHeaders: { - 'some-header': 'some-value', - }, - xsrf: { disableProtection: false, allowlist: [allowlistedTestPath] }, - requestId: { - allowFromAnyIp: true, - ipAllowlist: [], - }, - } as any) - ); + configService.atPath.mockImplementation((path) => { + if (path === 'server') { + return new BehaviorSubject({ + hosts: ['localhost'], + maxPayload: new ByteSizeValue(1024), + autoListen: true, + ssl: { + enabled: false, + }, + compression: { enabled: true }, + name: kibanaName, + customResponseHeaders: { + 'some-header': 'some-value', + }, + xsrf: { disableProtection: false, allowlist: [allowlistedTestPath] }, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + } as any); + } + if (path === 'externalUrl') { + return new BehaviorSubject({ + policy: [], + } as any); + } + if (path === 'csp') { + return new BehaviorSubject({} as any); + } + throw new Error(`Unexpected config path: ${path}`); + }); server = createHttpServer({ configService }); const serverSetup = await server.setup(setupDeps); diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index cdcbe513e1224..0a5cee5505ef1 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -32,28 +32,39 @@ const env = Env.createDefault(REPO_ROOT, getEnvOptions()); const logger = loggingSystemMock.create(); const configService = configServiceMock.create(); -configService.atPath.mockReturnValue( - new BehaviorSubject({ - hosts: ['localhost'], - maxPayload: new ByteSizeValue(1024), - autoListen: true, - ssl: { - enabled: false, - }, - compression: { enabled: true }, - xsrf: { - disableProtection: true, - allowlist: [], - }, - customResponseHeaders: {}, - requestId: { - allowFromAnyIp: true, - ipAllowlist: [], - }, - keepaliveTimeout: 120_000, - socketTimeout: 120_000, - } as any) -); +configService.atPath.mockImplementation((path) => { + if (path === 'server') { + return new BehaviorSubject({ + hosts: ['localhost'], + maxPayload: new ByteSizeValue(1024), + autoListen: true, + ssl: { + enabled: false, + }, + compression: { enabled: true }, + xsrf: { + disableProtection: true, + allowlist: [], + }, + customResponseHeaders: {}, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + keepaliveTimeout: 120_000, + socketTimeout: 120_000, + } as any); + } + if (path === 'externalUrl') { + return new BehaviorSubject({ + policy: [], + } as any); + } + if (path === 'csp') { + return new BehaviorSubject({} as any); + } + throw new Error(`Unexpected config path: ${path}`); +}); const defaultContext: CoreContext = { coreId, diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index afd7b0174d158..558fa20e0fd6b 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -30,6 +30,7 @@ import { OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { OnPostAuthHandler } from './lifecycle/on_post_auth'; import { OnPreResponseHandler } from './lifecycle/on_pre_response'; import { IBasePath } from './base_path_service'; +import { ExternalUrlConfig } from '../external_url'; import { PluginOpaqueId, RequestHandlerContext } from '..'; /** @@ -280,6 +281,7 @@ export interface InternalHttpServiceSetup extends Omit { auth: HttpServerSetup['auth']; server: HttpServerSetup['server']; + externalUrl: ExternalUrlConfig; createRouter: (path: string, plugin?: PluginOpaqueId) => IRouter; registerStaticDir: (path: string, dirPath: string) => void; getAuthHeaders: GetAuthHeaders; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 6abe067f24c8c..0f2761b67437d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -136,6 +136,7 @@ export { DeleteDocumentResponse, } from './elasticsearch'; export * from './elasticsearch/legacy/api_types'; +export { IExternalUrlConfig, IExternalUrlPolicy } from './external_url'; export { AuthenticationHandler, AuthHeaders, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 6da5d54869801..669286ccb2318 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -32,6 +32,7 @@ import { DevConfig, DevConfigType, config as devConfig } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig } from '../http'; import { Logger } from '../logging'; import { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig, LegacyVars } from './types'; +import { ExternalUrlConfigType, config as externalUrlConfig } from '../external_url'; import { CoreSetup, CoreStart } from '..'; interface LegacyKbnServer { @@ -84,8 +85,9 @@ export class LegacyService implements CoreService { .pipe(map((rawConfig) => new DevConfig(rawConfig))); this.httpConfig$ = combineLatest( configService.atPath(httpConfig.path), - configService.atPath(cspConfig.path) - ).pipe(map(([http, csp]) => new HttpConfig(http, csp))); + configService.atPath(cspConfig.path), + configService.atPath(externalUrlConfig.path) + ).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl))); } public async setupLegacyConfig() { diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index 553dc7c36e824..8cb704f09ce8c 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -5,6 +5,10 @@ - [Layouts](#layouts) - [Pattern layout](#pattern-layout) - [JSON layout](#json-layout) +- [Appenders](#appenders) + - [Rolling File Appender](#rolling-file-appender) + - [Triggering Policies](#triggering-policies) + - [Rolling strategies](#rolling-strategies) - [Configuration](#configuration) - [Usage](#usage) - [Logging config migration](#logging-config-migration) @@ -127,6 +131,138 @@ Outputs the process ID. With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context, message text and any other metadata that may be associated with the log message itself. +## Appenders + +### Rolling File Appender + +Similar to Log4j's `RollingFileAppender`, this appender will log into a file, and rotate it following a rolling +strategy when the configured policy triggers. + +#### Triggering Policies + +The triggering policy determines when a rolling should occur. + +There are currently two policies supported: `size-limit` and `time-interval`. + +##### SizeLimitTriggeringPolicy + +This policy will rotate the file when it reaches a predetermined size. + +```yaml +logging: + appenders: + rolling-file: + kind: rolling-file + path: /var/logs/kibana.log + policy: + kind: size-limit + size: 50mb + strategy: + //... + layout: + kind: pattern +``` + +The options are: + +- `size` + +the maximum size the log file should reach before a rollover should be performed. + +The default value is `100mb` + +##### TimeIntervalTriggeringPolicy + +This policy will rotate the file every given interval of time. + +```yaml +logging: + appenders: + rolling-file: + kind: rolling-file + path: /var/logs/kibana.log + policy: + kind: time-interval + interval: 10s + modulate: true + strategy: + //... + layout: + kind: pattern +``` + +The options are: + +- `interval` + +How often a rollover should occur. + +The default value is `24h` + +- `modulate` + +Whether the interval should be adjusted to cause the next rollover to occur on the interval boundary. + +For example, when true, if the interval is `4h` and the current hour is 3 am then the first rollover will occur at 4 am +and then next ones will occur at 8 am, noon, 4pm, etc. + +The default value is `true`. + +#### Rolling strategies + +The rolling strategy determines how the rollover should occur: both the naming of the rolled files, +and their retention policy. + +There is currently one strategy supported: `numeric`. + +##### NumericRollingStrategy + +This strategy will suffix the file with a given pattern when rolling, +and will retains a fixed amount of rolled files. + +```yaml +logging: + appenders: + rolling-file: + kind: rolling-file + path: /var/logs/kibana.log + policy: + // ... + strategy: + kind: numeric + pattern: '-%i' + max: 2 + layout: + kind: pattern +``` + +For example, with this configuration: + +- During the first rollover kibana.log is renamed to kibana-1.log. A new kibana.log file is created and starts + being written to. +- During the second rollover kibana-1.log is renamed to kibana-2.log and kibana.log is renamed to kibana-1.log. + A new kibana.log file is created and starts being written to. +- During the third and subsequent rollovers, kibana-2.log is deleted, kibana-1.log is renamed to kibana-2.log and + kibana.log is renamed to kibana-1.log. A new kibana.log file is created and starts being written to. + +The options are: + +- `pattern` + +The suffix to append to the file path when rolling. Must include `%i`, as this is the value +that will be converted to the file index. + +for example, with `path: /var/logs/kibana.log` and `pattern: '-%i'`, the created rolling files +will be `/var/logs/kibana-1.log`, `/var/logs/kibana-2.log`, and so on. + +The default value is `-%i` + +- `max` + +The maximum number of files to keep. Once this number is reached, oldest files will be deleted. + +The default value is `7` + ## Configuration As any configuration in the platform, logging configuration is validated against the predefined schema and if there are diff --git a/src/core/server/logging/appenders/appenders.test.ts b/src/core/server/logging/appenders/appenders.test.ts index 7cfd2158be338..831dbc9aa2707 100644 --- a/src/core/server/logging/appenders/appenders.test.ts +++ b/src/core/server/logging/appenders/appenders.test.ts @@ -19,10 +19,12 @@ import { mockCreateLayout } from './appenders.test.mocks'; +import { ByteSizeValue } from '@kbn/config-schema'; import { LegacyAppender } from '../../legacy/logging/appenders/legacy_appender'; import { Appenders } from './appenders'; import { ConsoleAppender } from './console/console_appender'; import { FileAppender } from './file/file_appender'; +import { RollingFileAppender } from './rolling_file/rolling_file_appender'; beforeEach(() => { mockCreateLayout.mockReset(); @@ -83,4 +85,13 @@ test('`create()` creates correct appender.', () => { }); expect(legacyAppender).toBeInstanceOf(LegacyAppender); + + const rollingFileAppender = Appenders.create({ + kind: 'rolling-file', + path: 'path', + layout: { highlight: true, kind: 'pattern', pattern: '' }, + strategy: { kind: 'numeric', max: 5, pattern: '%i' }, + policy: { kind: 'size-limit', size: ByteSizeValue.parse('15b') }, + }); + expect(rollingFileAppender).toBeInstanceOf(RollingFileAppender); }); diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts index 4e6920c50686c..aace9ed2b5db7 100644 --- a/src/core/server/logging/appenders/appenders.ts +++ b/src/core/server/logging/appenders/appenders.ts @@ -28,6 +28,10 @@ import { import { Layouts } from '../layouts/layouts'; import { ConsoleAppender, ConsoleAppenderConfig } from './console/console_appender'; import { FileAppender, FileAppenderConfig } from './file/file_appender'; +import { + RollingFileAppender, + RollingFileAppenderConfig, +} from './rolling_file/rolling_file_appender'; /** * Config schema for validting the shape of the `appenders` key in in {@link LoggerContextConfigType} or @@ -39,10 +43,15 @@ export const appendersSchema = schema.oneOf([ ConsoleAppender.configSchema, FileAppender.configSchema, LegacyAppender.configSchema, + RollingFileAppender.configSchema, ]); /** @public */ -export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig; +export type AppenderConfigType = + | ConsoleAppenderConfig + | FileAppenderConfig + | LegacyAppenderConfig + | RollingFileAppenderConfig; /** @internal */ export class Appenders { @@ -57,10 +66,10 @@ export class Appenders { switch (config.kind) { case 'console': return new ConsoleAppender(Layouts.create(config.layout)); - case 'file': return new FileAppender(Layouts.create(config.layout), config.path); - + case 'rolling-file': + return new RollingFileAppender(config); case 'legacy-appender': return new LegacyAppender(config.legacyLoggingConfig); diff --git a/src/core/server/logging/appenders/rolling_file/mocks.ts b/src/core/server/logging/appenders/rolling_file/mocks.ts new file mode 100644 index 0000000000000..2944235438688 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/mocks.ts @@ -0,0 +1,72 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import type { Layout } from '@kbn/logging'; +import type { RollingFileContext } from './rolling_file_context'; +import type { RollingFileManager } from './rolling_file_manager'; +import type { TriggeringPolicy } from './policies/policy'; +import type { RollingStrategy } from './strategies/strategy'; + +const createContextMock = (filePath: string) => { + const mock: jest.Mocked = { + currentFileSize: 0, + currentFileTime: 0, + filePath, + refreshFileInfo: jest.fn(), + }; + return mock; +}; + +const createStrategyMock = () => { + const mock: jest.Mocked = { + rollout: jest.fn(), + }; + return mock; +}; + +const createPolicyMock = () => { + const mock: jest.Mocked = { + isTriggeringEvent: jest.fn(), + }; + return mock; +}; + +const createLayoutMock = () => { + const mock: jest.Mocked = { + format: jest.fn(), + }; + return mock; +}; + +const createFileManagerMock = () => { + const mock: jest.Mocked> = { + write: jest.fn(), + closeStream: jest.fn(), + }; + return mock; +}; + +export const rollingFileAppenderMocks = { + createContext: createContextMock, + createStrategy: createStrategyMock, + createPolicy: createPolicyMock, + createLayout: createLayoutMock, + createFileManager: createFileManagerMock, +}; diff --git a/src/core/server/logging/appenders/rolling_file/policies/index.ts b/src/core/server/logging/appenders/rolling_file/policies/index.ts new file mode 100644 index 0000000000000..66eb7f039d37b --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/index.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import moment from 'moment-timezone'; +import { assertNever } from '@kbn/std'; +import { TriggeringPolicy } from './policy'; +import { RollingFileContext } from '../rolling_file_context'; +import { + sizeLimitTriggeringPolicyConfigSchema, + SizeLimitTriggeringPolicyConfig, + SizeLimitTriggeringPolicy, +} from './size_limit'; +import { + TimeIntervalTriggeringPolicyConfig, + TimeIntervalTriggeringPolicy, + timeIntervalTriggeringPolicyConfigSchema, +} from './time_interval'; + +export { TriggeringPolicy } from './policy'; + +/** + * Any of the existing policy's configuration + * + * See {@link SizeLimitTriggeringPolicyConfig} and {@link TimeIntervalTriggeringPolicyConfig} + */ +export type TriggeringPolicyConfig = + | SizeLimitTriggeringPolicyConfig + | TimeIntervalTriggeringPolicyConfig; + +const defaultPolicy: TimeIntervalTriggeringPolicyConfig = { + kind: 'time-interval', + interval: moment.duration(24, 'hour'), + modulate: true, +}; + +export const triggeringPolicyConfigSchema = schema.oneOf( + [sizeLimitTriggeringPolicyConfigSchema, timeIntervalTriggeringPolicyConfigSchema], + { defaultValue: defaultPolicy } +); + +export const createTriggeringPolicy = ( + config: TriggeringPolicyConfig, + context: RollingFileContext +): TriggeringPolicy => { + switch (config.kind) { + case 'size-limit': + return new SizeLimitTriggeringPolicy(config, context); + case 'time-interval': + return new TimeIntervalTriggeringPolicy(config, context); + default: + return assertNever(config); + } +}; diff --git a/src/core/server/logging/appenders/rolling_file/policies/policy.ts b/src/core/server/logging/appenders/rolling_file/policies/policy.ts new file mode 100644 index 0000000000000..eeded68711829 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/policy.ts @@ -0,0 +1,30 @@ +/* + * 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 { LogRecord } from '@kbn/logging'; + +/** + * A policy used to determinate when a rollout should be performed. + */ +export interface TriggeringPolicy { + /** + * Determines whether a rollover should occur before logging given record. + **/ + isTriggeringEvent(record: LogRecord): boolean; +} diff --git a/tasks/config/watch.js b/src/core/server/logging/appenders/rolling_file/policies/size_limit/index.ts similarity index 84% rename from tasks/config/watch.js rename to src/core/server/logging/appenders/rolling_file/policies/size_limit/index.ts index b132b7e5f8087..7502eb4fb90c0 100644 --- a/tasks/config/watch.js +++ b/src/core/server/logging/appenders/rolling_file/policies/size_limit/index.ts @@ -17,9 +17,8 @@ * under the License. */ -module.exports = { - peg: { - files: ['src/legacy/utils/kuery/ast/*.peg'], - tasks: ['peg'], - }, -}; +export { + SizeLimitTriggeringPolicy, + SizeLimitTriggeringPolicyConfig, + sizeLimitTriggeringPolicyConfigSchema, +} from './size_limit_policy'; diff --git a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts new file mode 100644 index 0000000000000..f54ca8d2f1f8a --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { ByteSizeValue } from '@kbn/config-schema'; +import { LogRecord, LogLevel } from '@kbn/logging'; +import { SizeLimitTriggeringPolicy } from './size_limit_policy'; +import { RollingFileContext } from '../../rolling_file_context'; + +describe('SizeLimitTriggeringPolicy', () => { + let context: RollingFileContext; + + const createPolicy = (size: ByteSizeValue) => + new SizeLimitTriggeringPolicy({ kind: 'size-limit', size }, context); + + const createLogRecord = (parts: Partial = {}): LogRecord => ({ + timestamp: new Date(), + level: LogLevel.Info, + context: 'context', + message: 'just a log', + pid: 42, + ...parts, + }); + + const isTriggering = ({ fileSize, maxSize }: { maxSize: string; fileSize: string }) => { + const policy = createPolicy(ByteSizeValue.parse(maxSize)); + context.currentFileSize = ByteSizeValue.parse(fileSize).getValueInBytes(); + return policy.isTriggeringEvent(createLogRecord()); + }; + + beforeEach(() => { + context = new RollingFileContext('foo.log'); + }); + + it('triggers a rollover when the file size exceeds the max size', () => { + expect( + isTriggering({ + fileSize: '70b', + maxSize: '50b', + }) + ).toBeTruthy(); + }); + + it('triggers a rollover when the file size equals the max size', () => { + expect( + isTriggering({ + fileSize: '20b', + maxSize: '20b', + }) + ).toBeTruthy(); + }); + + it('does not triggers a rollover when the file size did not rea h the max size', () => { + expect( + isTriggering({ + fileSize: '20b', + maxSize: '50b', + }) + ).toBeFalsy(); + }); +}); diff --git a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts new file mode 100644 index 0000000000000..cf3e90d0fbce1 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.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 { schema, ByteSizeValue } from '@kbn/config-schema'; +import { LogRecord } from '@kbn/logging'; +import { RollingFileContext } from '../../rolling_file_context'; +import { TriggeringPolicy } from '../policy'; + +export interface SizeLimitTriggeringPolicyConfig { + kind: 'size-limit'; + + /** + * The minimum size the file must have to roll over. + */ + size: ByteSizeValue; +} + +export const sizeLimitTriggeringPolicyConfigSchema = schema.object({ + kind: schema.literal('size-limit'), + size: schema.byteSize({ min: '1b', defaultValue: '100mb' }), +}); + +/** + * A triggering policy based on a fixed size limit. + * + * Will trigger a rollover when the current log size exceed the + * given {@link SizeLimitTriggeringPolicyConfig.size | size}. + */ +export class SizeLimitTriggeringPolicy implements TriggeringPolicy { + private readonly maxFileSize: number; + + constructor( + config: SizeLimitTriggeringPolicyConfig, + private readonly context: RollingFileContext + ) { + this.maxFileSize = config.size.getValueInBytes(); + } + + isTriggeringEvent(record: LogRecord): boolean { + return this.context.currentFileSize >= this.maxFileSize; + } +} diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/get_next_rolling_time.test.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/get_next_rolling_time.test.ts new file mode 100644 index 0000000000000..66de78a89d7f8 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/get_next_rolling_time.test.ts @@ -0,0 +1,86 @@ +/* + * 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-timezone'; +import { schema } from '@kbn/config-schema'; +import { getNextRollingTime } from './get_next_rolling_time'; + +const format = 'YYYY-MM-DD HH:mm:ss:SSS'; + +const formattedRollingTime = (date: string, duration: string, modulate: boolean) => + moment( + getNextRollingTime( + moment(date, format).toDate().getTime(), + schema.duration().validate(duration), + modulate + ) + ).format(format); + +describe('getNextRollingTime', () => { + describe('when `modulate` is false', () => { + it('increments the current time by the interval', () => { + expect(formattedRollingTime('2010-10-20 04:27:12:000', '15m', false)).toEqual( + '2010-10-20 04:42:12:000' + ); + + expect(formattedRollingTime('2010-02-12 04:27:12:000', '24h', false)).toEqual( + '2010-02-13 04:27:12:000' + ); + + expect(formattedRollingTime('2010-02-17 06:34:55', '2d', false)).toEqual( + '2010-02-19 06:34:55:000' + ); + }); + }); + + describe('when `modulate` is true', () => { + it('increments the current time to reach the next boundary', () => { + expect(formattedRollingTime('2010-10-20 04:27:12:512', '30m', true)).toEqual( + '2010-10-20 04:30:00:000' + ); + expect(formattedRollingTime('2010-10-20 04:27:12:512', '6h', true)).toEqual( + '2010-10-20 06:00:00:000' + ); + expect(formattedRollingTime('2010-10-20 04:27:12:512', '1w', true)).toEqual( + '2010-10-24 00:00:00:000' + ); + }); + + it('works when on the edge of a boundary', () => { + expect(formattedRollingTime('2010-10-20 06:00:00:000', '6h', true)).toEqual( + '2010-10-20 12:00:00:000' + ); + expect(formattedRollingTime('2010-10-14 00:00:00:000', '1d', true)).toEqual( + '2010-10-15 00:00:00:000' + ); + expect(formattedRollingTime('2010-01-03 00:00:00:000', '2w', true)).toEqual( + '2010-01-17 00:00:00:000' + ); + }); + + it('increments a higher unit when necessary', () => { + expect(formattedRollingTime('2010-10-20 21:00:00:000', '9h', true)).toEqual( + '2010-10-21 03:00:00:000' + ); + expect(formattedRollingTime('2010-12-31 21:00:00:000', '4d', true)).toEqual( + '2011-01-03 00:00:00:000' + ); + }); + }); +}); diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/get_next_rolling_time.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/get_next_rolling_time.ts new file mode 100644 index 0000000000000..11cbace5ce043 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/get_next_rolling_time.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 moment, { Duration } from 'moment-timezone'; +import { getHighestTimeUnit } from './utils'; + +/** + * Return the next rollout time, given current time and rollout interval + */ +export const getNextRollingTime = ( + currentTime: number, + interval: Duration, + modulate: boolean +): number => { + if (modulate) { + const incrementedUnit = getHighestTimeUnit(interval); + const currentMoment = moment(currentTime); + const increment = + interval.get(incrementedUnit) - + (currentMoment.get(incrementedUnit) % interval.get(incrementedUnit)); + const incrementInMs = moment.duration(increment, incrementedUnit).asMilliseconds(); + return currentMoment.startOf(incrementedUnit).toDate().getTime() + incrementInMs; + } else { + return currentTime + interval.asMilliseconds(); + } +}; diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/index.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/index.ts new file mode 100644 index 0000000000000..481b7a77d8463 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { + TimeIntervalTriggeringPolicy, + TimeIntervalTriggeringPolicyConfig, + timeIntervalTriggeringPolicyConfigSchema, +} from './time_interval_policy'; diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.mocks.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.mocks.ts new file mode 100644 index 0000000000000..5383f55bb19e5 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.mocks.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const getNextRollingTimeMock = jest.fn(); +jest.doMock('./get_next_rolling_time', () => ({ getNextRollingTime: getNextRollingTimeMock })); diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts new file mode 100644 index 0000000000000..3f06883da8884 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts @@ -0,0 +1,147 @@ +/* + * 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 { getNextRollingTimeMock } from './time_interval_policy.test.mocks'; +import moment from 'moment-timezone'; +import { LogLevel, LogRecord } from '@kbn/logging'; +import { schema } from '@kbn/config-schema'; +import { + TimeIntervalTriggeringPolicy, + TimeIntervalTriggeringPolicyConfig, +} from './time_interval_policy'; +import { RollingFileContext } from '../../rolling_file_context'; + +const format = 'YYYY-MM-DD HH:mm:ss'; + +describe('TimeIntervalTriggeringPolicy', () => { + afterEach(() => { + getNextRollingTimeMock.mockReset(); + jest.restoreAllMocks(); + }); + + const createLogRecord = (timestamp: Date): LogRecord => ({ + timestamp, + level: LogLevel.Info, + context: 'context', + message: 'just a log', + pid: 42, + }); + + const createContext = (currentFileTime: number = Date.now()): RollingFileContext => { + const context = new RollingFileContext('foo.log'); + context.currentFileTime = currentFileTime; + return context; + }; + + const createConfig = ( + interval: string = '15m', + modulate: boolean = false + ): TimeIntervalTriggeringPolicyConfig => ({ + kind: 'time-interval', + interval: schema.duration().validate(interval), + modulate, + }); + + it('calls `getNextRollingTime` during construction with the correct parameters', () => { + const date = moment('2010-10-20 04:27:12', format).toDate(); + const context = createContext(date.getTime()); + const config = createConfig('15m', true); + + new TimeIntervalTriggeringPolicy(config, context); + + expect(getNextRollingTimeMock).toHaveBeenCalledTimes(1); + expect(getNextRollingTimeMock).toHaveBeenCalledWith( + context.currentFileTime, + config.interval, + config.modulate + ); + }); + + it('calls `getNextRollingTime` with the current time if `context.currentFileTime` is not set', () => { + const currentTime = moment('2018-06-15 04:27:12', format).toDate().getTime(); + jest.spyOn(Date, 'now').mockReturnValue(currentTime); + const context = createContext(0); + const config = createConfig('15m', true); + + new TimeIntervalTriggeringPolicy(config, context); + + expect(getNextRollingTimeMock).toHaveBeenCalledTimes(1); + expect(getNextRollingTimeMock).toHaveBeenCalledWith( + currentTime, + config.interval, + config.modulate + ); + }); + + describe('#isTriggeringEvent', () => { + it('returns true if the event time is after the nextRolloverTime', () => { + const eventDate = moment('2010-10-20 04:43:12', format).toDate(); + const nextRolloverDate = moment('2010-10-20 04:00:00', format).toDate(); + + getNextRollingTimeMock.mockReturnValue(nextRolloverDate.getTime()); + + const policy = new TimeIntervalTriggeringPolicy(createConfig(), createContext()); + + expect(policy.isTriggeringEvent(createLogRecord(eventDate))).toBeTruthy(); + }); + + it('returns true if the event time is exactly the nextRolloverTime', () => { + const eventDate = moment('2010-10-20 04:00:00', format).toDate(); + const nextRolloverDate = moment('2010-10-20 04:00:00', format).toDate(); + + getNextRollingTimeMock.mockReturnValue(nextRolloverDate.getTime()); + + const policy = new TimeIntervalTriggeringPolicy(createConfig(), createContext()); + + expect(policy.isTriggeringEvent(createLogRecord(eventDate))).toBeTruthy(); + }); + + it('returns false if the event time is before the nextRolloverTime', () => { + const eventDate = moment('2010-10-20 03:47:12', format).toDate(); + const nextRolloverDate = moment('2010-10-20 04:00:00', format).toDate(); + + getNextRollingTimeMock.mockReturnValue(nextRolloverDate.getTime()); + + const policy = new TimeIntervalTriggeringPolicy(createConfig(), createContext()); + + expect(policy.isTriggeringEvent(createLogRecord(eventDate))).toBeFalsy(); + }); + + it('refreshes its `nextRolloverTime` when returning true', () => { + const eventDate = moment('2010-10-20 04:43:12', format).toDate(); + const firstRollOverDate = moment('2010-10-20 04:00:00', format).toDate(); + const nextRollOverDate = moment('2010-10-20 08:00:00', format).toDate(); + + getNextRollingTimeMock + // constructor call + .mockReturnValueOnce(firstRollOverDate.getTime()) + // call performed during `isTriggeringEvent` to refresh the rolling time + .mockReturnValueOnce(nextRollOverDate.getTime()); + + const policy = new TimeIntervalTriggeringPolicy(createConfig(), createContext()); + + const logRecord = createLogRecord(eventDate); + + // rollingDate is firstRollOverDate + expect(policy.isTriggeringEvent(logRecord)).toBeTruthy(); + // rollingDate should be nextRollOverDate + expect(policy.isTriggeringEvent(logRecord)).toBeFalsy(); + }); + }); +}); diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts new file mode 100644 index 0000000000000..330a74b03f20e --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts @@ -0,0 +1,96 @@ +/* + * 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 { Duration } from 'moment-timezone'; +import { schema } from '@kbn/config-schema'; +import { LogRecord } from '@kbn/logging'; +import { RollingFileContext } from '../../rolling_file_context'; +import { TriggeringPolicy } from '../policy'; +import { getNextRollingTime } from './get_next_rolling_time'; +import { isValidRolloverInterval } from './utils'; + +export interface TimeIntervalTriggeringPolicyConfig { + kind: 'time-interval'; + + /** + * How often a rollover should occur. + * + * @remarks + * Due to how modulate rolling works, it is required to have an integer value for the highest time unit + * of the duration (you can't overflow to a higher unit). + * For example, `15m` and `4h` are valid values , but `90m` is not (as it is `1.5h`). + */ + interval: Duration; + + /** + * Indicates whether the interval should be adjusted to cause the next rollover to occur on the interval boundary. + * + * For example, if the interval is `4h` and the current hour is 3 am then + * the first rollover will occur at 4 am and then next ones will occur at 8 am, noon, 4pm, etc. + * The default value is true. + */ + modulate: boolean; +} + +export const timeIntervalTriggeringPolicyConfigSchema = schema.object({ + kind: schema.literal('time-interval'), + interval: schema.duration({ + defaultValue: '24h', + validate: (interval) => { + if (!isValidRolloverInterval(interval)) { + return 'Interval value cannot overflow to a higher time unit.'; + } + }, + }), + modulate: schema.boolean({ defaultValue: true }), +}); + +/** + * A triggering policy based on a fixed time interval + */ +export class TimeIntervalTriggeringPolicy implements TriggeringPolicy { + /** + * milliseconds timestamp of when the next rollover should occur. + */ + private nextRolloverTime: number; + + constructor( + private readonly config: TimeIntervalTriggeringPolicyConfig, + context: RollingFileContext + ) { + this.nextRolloverTime = getNextRollingTime( + context.currentFileTime || Date.now(), + config.interval, + config.modulate + ); + } + + isTriggeringEvent(record: LogRecord): boolean { + const eventTime = record.timestamp.getTime(); + if (eventTime >= this.nextRolloverTime) { + this.nextRolloverTime = getNextRollingTime( + eventTime, + this.config.interval, + this.config.modulate + ); + return true; + } + return false; + } +} diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/utils.test.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/utils.test.ts new file mode 100644 index 0000000000000..1b9517f6ade3c --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/utils.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { getHighestTimeUnit, isValidRolloverInterval } from './utils'; + +const duration = (raw: string) => schema.duration().validate(raw); + +describe('getHighestTimeUnit', () => { + it('returns the highest time unit of the duration', () => { + expect(getHighestTimeUnit(duration('500ms'))).toEqual('millisecond'); + expect(getHighestTimeUnit(duration('30s'))).toEqual('second'); + expect(getHighestTimeUnit(duration('15m'))).toEqual('minute'); + expect(getHighestTimeUnit(duration('12h'))).toEqual('hour'); + expect(getHighestTimeUnit(duration('4d'))).toEqual('day'); + expect(getHighestTimeUnit(duration('3w'))).toEqual('week'); + expect(getHighestTimeUnit(duration('7M'))).toEqual('month'); + expect(getHighestTimeUnit(duration('7Y'))).toEqual('year'); + }); + + it('handles overflows', () => { + expect(getHighestTimeUnit(duration('2000ms'))).toEqual('second'); + expect(getHighestTimeUnit(duration('90s'))).toEqual('minute'); + expect(getHighestTimeUnit(duration('75m'))).toEqual('hour'); + expect(getHighestTimeUnit(duration('36h'))).toEqual('day'); + expect(getHighestTimeUnit(duration('9d'))).toEqual('week'); + expect(getHighestTimeUnit(duration('15w'))).toEqual('month'); + expect(getHighestTimeUnit(duration('23M'))).toEqual('year'); + }); +}); + +describe('isValidRolloverInterval', () => { + it('returns true if the interval does not overflow', () => { + expect(isValidRolloverInterval(duration('500ms'))).toEqual(true); + expect(isValidRolloverInterval(duration('30s'))).toEqual(true); + expect(isValidRolloverInterval(duration('15m'))).toEqual(true); + expect(isValidRolloverInterval(duration('12h'))).toEqual(true); + expect(isValidRolloverInterval(duration('4d'))).toEqual(true); + expect(isValidRolloverInterval(duration('3w'))).toEqual(true); + expect(isValidRolloverInterval(duration('7M'))).toEqual(true); + expect(isValidRolloverInterval(duration('7Y'))).toEqual(true); + }); + + it('returns false if the interval overflows to a non integer value', () => { + expect(isValidRolloverInterval(duration('2500ms'))).toEqual(false); + expect(isValidRolloverInterval(duration('90s'))).toEqual(false); + expect(isValidRolloverInterval(duration('75m'))).toEqual(false); + expect(isValidRolloverInterval(duration('36h'))).toEqual(false); + expect(isValidRolloverInterval(duration('9d'))).toEqual(false); + expect(isValidRolloverInterval(duration('15w'))).toEqual(false); + expect(isValidRolloverInterval(duration('23M'))).toEqual(false); + }); + + it('returns true if the interval overflows to an integer value', () => { + expect(isValidRolloverInterval(duration('2000ms'))).toEqual(true); + expect(isValidRolloverInterval(duration('120s'))).toEqual(true); + expect(isValidRolloverInterval(duration('240m'))).toEqual(true); + expect(isValidRolloverInterval(duration('48h'))).toEqual(true); + expect(isValidRolloverInterval(duration('14d'))).toEqual(true); + expect(isValidRolloverInterval(duration('24M'))).toEqual(true); + }); +}); diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/utils.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/utils.ts new file mode 100644 index 0000000000000..ca2cbf31dfc6f --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/utils.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Duration, unitOfTime } from 'moment-timezone'; + +/** + * Returns the highest time unit of the given duration + * (the highest unit with a value higher of equal to 1) + * + * @example + * ``` + * getHighestTimeUnit(moment.duration(4, 'day')) + * // 'day' + * getHighestTimeUnit(moment.duration(90, 'minute')) + * // 'hour' - 90min = 1.5h + * getHighestTimeUnit(moment.duration(30, 'minute')) + * // 'minute' - 30min = 0,5h + * ``` + */ +export const getHighestTimeUnit = (duration: Duration): unitOfTime.Base => { + if (duration.asYears() >= 1) { + return 'year'; + } + if (duration.asMonths() >= 1) { + return 'month'; + } + if (duration.asWeeks() >= 1) { + return 'week'; + } + if (duration.asDays() >= 1) { + return 'day'; + } + if (duration.asHours() >= 1) { + return 'hour'; + } + if (duration.asMinutes() >= 1) { + return 'minute'; + } + if (duration.asSeconds() >= 1) { + return 'second'; + } + return 'millisecond'; +}; + +/** + * Returns true if the given duration is valid to be used with by the {@link TimeIntervalTriggeringPolicy | policy} + * + * See {@link TimeIntervalTriggeringPolicyConfig.interval} for rules and reasons around this validation. + */ +export const isValidRolloverInterval = (duration: Duration): boolean => { + const highestUnit = getHighestTimeUnit(duration); + const asHighestUnit = duration.as(highestUnit); + return Number.isInteger(asHighestUnit); +}; diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.mocks.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.mocks.ts new file mode 100644 index 0000000000000..c84cf09fffe89 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.mocks.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 { schema } from '@kbn/config-schema'; + +export const LayoutsMock = { + create: jest.fn(), + configSchema: schema.any(), +}; +jest.doMock('../../layouts/layouts', () => ({ + Layouts: LayoutsMock, +})); + +export const createTriggeringPolicyMock = jest.fn(); +jest.doMock('./policies', () => ({ + triggeringPolicyConfigSchema: schema.any(), + createTriggeringPolicy: createTriggeringPolicyMock, +})); + +export const createRollingStrategyMock = jest.fn(); +jest.doMock('./strategies', () => ({ + rollingStrategyConfigSchema: schema.any(), + createRollingStrategy: createRollingStrategyMock, +})); + +export const RollingFileManagerMock = jest.fn(); +jest.doMock('./rolling_file_manager', () => ({ + RollingFileManager: RollingFileManagerMock, +})); + +export const RollingFileContextMock = jest.fn(); +jest.doMock('./rolling_file_context', () => ({ + RollingFileContext: RollingFileContextMock, +})); + +export const resetAllMocks = () => { + LayoutsMock.create.mockReset(); + createTriggeringPolicyMock.mockReset(); + createRollingStrategyMock.mockReset(); + RollingFileManagerMock.mockReset(); + RollingFileContextMock.mockReset(); +}; diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts new file mode 100644 index 0000000000000..96051903e16e2 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts @@ -0,0 +1,275 @@ +/* + * 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 { + createRollingStrategyMock, + createTriggeringPolicyMock, + LayoutsMock, + resetAllMocks, + RollingFileContextMock, + RollingFileManagerMock, +} from './rolling_file_appender.test.mocks'; +import { rollingFileAppenderMocks } from './mocks'; +import moment from 'moment-timezone'; +import { LogLevel, LogRecord } from '@kbn/logging'; +import { RollingFileAppender, RollingFileAppenderConfig } from './rolling_file_appender'; + +const config: RollingFileAppenderConfig = { + kind: 'rolling-file', + path: '/var/log/kibana.log', + layout: { + kind: 'pattern', + pattern: '%message', + highlight: false, + }, + policy: { + kind: 'time-interval', + interval: moment.duration(4, 'hour'), + modulate: true, + }, + strategy: { + kind: 'numeric', + max: 5, + pattern: '-%i', + }, +}; + +const createLogRecord = (parts: Partial = {}): LogRecord => ({ + timestamp: new Date(), + level: LogLevel.Info, + context: 'context', + message: 'just a log', + pid: 42, + ...parts, +}); + +const nextTick = () => new Promise((resolve) => setTimeout(resolve, 10)); + +const createPromiseResolver = () => { + let resolve: () => void; + let reject: () => void; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + return { + promise, + resolve: resolve!, + reject: reject!, + }; +}; + +describe('RollingFileAppender', () => { + let appender: RollingFileAppender; + + let layout: ReturnType; + let strategy: ReturnType; + let policy: ReturnType; + let context: ReturnType; + let fileManager: ReturnType; + + beforeEach(() => { + layout = rollingFileAppenderMocks.createLayout(); + LayoutsMock.create.mockReturnValue(layout); + + policy = rollingFileAppenderMocks.createPolicy(); + createTriggeringPolicyMock.mockReturnValue(policy); + + strategy = rollingFileAppenderMocks.createStrategy(); + createRollingStrategyMock.mockReturnValue(strategy); + + context = rollingFileAppenderMocks.createContext('file-path'); + RollingFileContextMock.mockImplementation(() => context); + + fileManager = rollingFileAppenderMocks.createFileManager(); + RollingFileManagerMock.mockImplementation(() => fileManager); + + appender = new RollingFileAppender(config); + }); + + afterAll(() => { + resetAllMocks(); + }); + + it('constructs its delegates with the correct parameters', () => { + expect(RollingFileContextMock).toHaveBeenCalledTimes(1); + expect(RollingFileContextMock).toHaveBeenCalledWith(config.path); + + expect(RollingFileManagerMock).toHaveBeenCalledTimes(1); + expect(RollingFileManagerMock).toHaveBeenCalledWith(context); + + expect(LayoutsMock.create).toHaveBeenCalledTimes(1); + expect(LayoutsMock.create).toHaveBeenCalledWith(config.layout); + + expect(createTriggeringPolicyMock).toHaveBeenCalledTimes(1); + expect(createTriggeringPolicyMock).toHaveBeenCalledWith(config.policy, context); + + expect(createRollingStrategyMock).toHaveBeenCalledTimes(1); + expect(createRollingStrategyMock).toHaveBeenCalledWith(config.strategy, context); + }); + + describe('#append', () => { + describe('when rollout is not needed', () => { + beforeEach(() => { + policy.isTriggeringEvent.mockReturnValue(false); + }); + + it('calls `layout.format` with the message', () => { + const log1 = createLogRecord({ message: '1' }); + const log2 = createLogRecord({ message: '2' }); + + appender.append(log1); + + expect(layout.format).toHaveBeenCalledTimes(1); + expect(layout.format).toHaveBeenCalledWith(log1); + + appender.append(log2); + + expect(layout.format).toHaveBeenCalledTimes(2); + expect(layout.format).toHaveBeenCalledWith(log2); + }); + + it('calls `fileManager.write` with the formatted message', () => { + layout.format.mockImplementation(({ message }) => message); + + const log1 = createLogRecord({ message: '1' }); + const log2 = createLogRecord({ message: '2' }); + + appender.append(log1); + + expect(fileManager.write).toHaveBeenCalledTimes(1); + expect(fileManager.write).toHaveBeenCalledWith('1\n'); + + appender.append(log2); + + expect(fileManager.write).toHaveBeenCalledTimes(2); + expect(fileManager.write).toHaveBeenCalledWith('2\n'); + }); + }); + + describe('when rollout is needed', () => { + beforeEach(() => { + policy.isTriggeringEvent.mockReturnValueOnce(true).mockReturnValue(false); + }); + + it('does not log the event triggering the rollout', () => { + const log = createLogRecord({ message: '1' }); + appender.append(log); + + expect(layout.format).not.toHaveBeenCalled(); + expect(fileManager.write).not.toHaveBeenCalled(); + }); + + it('triggers the rollout', () => { + const log = createLogRecord({ message: '1' }); + appender.append(log); + + expect(strategy.rollout).toHaveBeenCalledTimes(1); + }); + + it('closes the manager stream once the rollout is complete', async () => { + const { promise, resolve } = createPromiseResolver(); + strategy.rollout.mockReturnValue(promise); + + const log = createLogRecord({ message: '1' }); + appender.append(log); + + expect(fileManager.closeStream).not.toHaveBeenCalled(); + + resolve(); + await nextTick(); + + expect(fileManager.closeStream).toHaveBeenCalledTimes(1); + }); + + it('logs the event once the rollout is complete', async () => { + const { promise, resolve } = createPromiseResolver(); + strategy.rollout.mockReturnValue(promise); + + const log = createLogRecord({ message: '1' }); + appender.append(log); + + expect(fileManager.write).not.toHaveBeenCalled(); + + resolve(); + await nextTick(); + + expect(fileManager.write).toHaveBeenCalledTimes(1); + }); + + it('logs any pending events once the rollout is complete', async () => { + const { promise, resolve } = createPromiseResolver(); + strategy.rollout.mockReturnValue(promise); + + appender.append(createLogRecord({ message: '1' })); + appender.append(createLogRecord({ message: '2' })); + appender.append(createLogRecord({ message: '3' })); + + expect(fileManager.write).not.toHaveBeenCalled(); + + resolve(); + await nextTick(); + + expect(fileManager.write).toHaveBeenCalledTimes(3); + }); + }); + }); + + describe('#dispose', () => { + it('closes the file manager', async () => { + await appender.dispose(); + + expect(fileManager.closeStream).toHaveBeenCalledTimes(1); + }); + + it('noops if called multiple times', async () => { + await appender.dispose(); + + expect(fileManager.closeStream).toHaveBeenCalledTimes(1); + + await appender.dispose(); + + expect(fileManager.closeStream).toHaveBeenCalledTimes(1); + }); + + it('waits until the rollout completes if a rollout was in progress', async () => { + expect.assertions(1); + + const { promise, resolve } = createPromiseResolver(); + let rolloutComplete = false; + + strategy.rollout.mockReturnValue( + promise.then(() => { + rolloutComplete = true; + }) + ); + + appender.append(createLogRecord({ message: '1' })); + + const dispose = appender.dispose().then(() => { + expect(rolloutComplete).toEqual(true); + }); + + resolve(); + + await Promise.all([dispose, promise]); + }); + }); +}); diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts new file mode 100644 index 0000000000000..3ec5c62aec3bb --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts @@ -0,0 +1,174 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { LogRecord, Layout, DisposableAppender } from '@kbn/logging'; +import { Layouts, LayoutConfigType } from '../../layouts/layouts'; +import { BufferAppender } from '../buffer/buffer_appender'; +import { + TriggeringPolicyConfig, + createTriggeringPolicy, + triggeringPolicyConfigSchema, + TriggeringPolicy, +} from './policies'; +import { + RollingStrategy, + createRollingStrategy, + RollingStrategyConfig, + rollingStrategyConfigSchema, +} from './strategies'; +import { RollingFileManager } from './rolling_file_manager'; +import { RollingFileContext } from './rolling_file_context'; + +export interface RollingFileAppenderConfig { + kind: 'rolling-file'; + /** + * The layout to use when writing log entries + */ + layout: LayoutConfigType; + /** + * The absolute path of the file to write to. + */ + path: string; + /** + * The {@link TriggeringPolicy | policy} to use to determine if a rollover should occur. + */ + policy: TriggeringPolicyConfig; + /** + * The {@link RollingStrategy | rollout strategy} to use for rolling. + */ + strategy: RollingStrategyConfig; +} + +/** + * Appender that formats all the `LogRecord` instances it receives and writes them to the specified file. + * @internal + */ +export class RollingFileAppender implements DisposableAppender { + public static configSchema = schema.object({ + kind: schema.literal('rolling-file'), + layout: Layouts.configSchema, + path: schema.string(), + policy: triggeringPolicyConfigSchema, + strategy: rollingStrategyConfigSchema, + }); + + private isRolling = false; + private disposed = false; + private rollingPromise?: Promise; + + private readonly layout: Layout; + private readonly context: RollingFileContext; + private readonly fileManager: RollingFileManager; + private readonly policy: TriggeringPolicy; + private readonly strategy: RollingStrategy; + private readonly buffer: BufferAppender; + + constructor(config: RollingFileAppenderConfig) { + this.context = new RollingFileContext(config.path); + this.context.refreshFileInfo(); + this.fileManager = new RollingFileManager(this.context); + this.layout = Layouts.create(config.layout); + this.policy = createTriggeringPolicy(config.policy, this.context); + this.strategy = createRollingStrategy(config.strategy, this.context); + this.buffer = new BufferAppender(); + } + + /** + * Formats specified `record` and writes it to the specified file. If the record + * would trigger a rollover, it will be performed before the effective write operation. + */ + public append(record: LogRecord) { + // if we are currently rolling the files, push the log record + // into the buffer, which will be flushed once rolling is complete + if (this.isRolling) { + this.buffer.append(record); + return; + } + if (this.needRollout(record)) { + this.buffer.append(record); + this.rollingPromise = this.performRollout(); + return; + } + + this._writeToFile(record); + } + + private _writeToFile(record: LogRecord) { + this.fileManager.write(`${this.layout.format(record)}\n`); + } + + /** + * Disposes the appender. + * If a rollout is currently in progress, it will be awaited. + */ + public async dispose() { + if (this.disposed) { + return; + } + this.disposed = true; + if (this.rollingPromise) { + await this.rollingPromise; + } + await this.buffer.dispose(); + await this.fileManager.closeStream(); + } + + private async performRollout() { + if (this.isRolling) { + return; + } + this.isRolling = true; + try { + await this.strategy.rollout(); + await this.fileManager.closeStream(); + } catch (e) { + // eslint-disable-next-line no-console + console.error('[RollingFileAppender]: error while rolling file: ', e); + } + this.rollingPromise = undefined; + this.isRolling = false; + this.flushBuffer(); + } + + private flushBuffer() { + const pendingLogs = this.buffer.flush(); + // in some extreme scenarios, `dispose` can be called during a rollover + // where the internal buffered logs would trigger another rollover + // (rollover started, logs keep coming and got buffered, dispose is called, rollover ends and we then flush) + // this would cause a second rollover that would not be awaited + // and could result in a race with the newly created appender + // that would also be performing a rollover. + // so if we are disposed, we just flush the buffer directly to the file instead to avoid loosing the entries. + for (const log of pendingLogs) { + if (this.disposed) { + this._writeToFile(log); + } else { + this.append(log); + } + } + } + + /** + * Checks if the current event should trigger a rollout + */ + private needRollout(record: LogRecord) { + return this.policy.isTriggeringEvent(record); + } +} diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_context.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_context.ts new file mode 100644 index 0000000000000..ed3b30cea2330 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_context.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 { statSync } from 'fs'; + +/** + * Context shared between the rolling file manager, policy and strategy. + */ +export class RollingFileContext { + constructor(public readonly filePath: string) {} + /** + * The size of the currently opened file. + */ + public currentFileSize: number = 0; + /** + * The time the currently opened file was created. + */ + public currentFileTime: number = 0; + + public refreshFileInfo() { + try { + const { birthtime, size } = statSync(this.filePath); + this.currentFileTime = birthtime.getTime(); + this.currentFileSize = size; + } catch (e) { + if (e.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.error('[RollingFileAppender] error accessing the log file', e); + } + this.currentFileTime = Date.now(); + this.currentFileSize = 0; + } + } +} diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_manager.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_manager.ts new file mode 100644 index 0000000000000..c2224de7db6fb --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_manager.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 { createWriteStream, WriteStream } from 'fs'; +import { RollingFileContext } from './rolling_file_context'; + +/** + * Delegate of the {@link RollingFileAppender} used to manage the log file access + */ +export class RollingFileManager { + private readonly filePath; + private outputStream?: WriteStream; + + constructor(private readonly context: RollingFileContext) { + this.filePath = context.filePath; + } + + write(chunk: string) { + const stream = this.ensureStreamOpen(); + this.context.currentFileSize += Buffer.byteLength(chunk, 'utf8'); + stream.write(chunk); + } + + async closeStream() { + return new Promise((resolve) => { + if (this.outputStream === undefined) { + return resolve(); + } + this.outputStream.end(() => { + this.outputStream = undefined; + resolve(); + }); + }); + } + + private ensureStreamOpen() { + if (this.outputStream === undefined) { + this.outputStream = createWriteStream(this.filePath, { + encoding: 'utf8', + flags: 'a', + }); + // refresh the file meta in case it was not initialized yet. + this.context.refreshFileInfo(); + } + return this.outputStream!; + } +} diff --git a/src/core/server/logging/appenders/rolling_file/strategies/index.ts b/src/core/server/logging/appenders/rolling_file/strategies/index.ts new file mode 100644 index 0000000000000..e51a16a0026a8 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/index.ts @@ -0,0 +1,47 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { RollingStrategy } from './strategy'; +import { + NumericRollingStrategy, + NumericRollingStrategyConfig, + numericRollingStrategyConfigSchema, +} from './numeric'; +import { RollingFileContext } from '../rolling_file_context'; + +export { RollingStrategy } from './strategy'; +export type RollingStrategyConfig = NumericRollingStrategyConfig; + +const defaultStrategy: NumericRollingStrategyConfig = { + kind: 'numeric', + pattern: '-%i', + max: 7, +}; + +export const rollingStrategyConfigSchema = schema.oneOf([numericRollingStrategyConfigSchema], { + defaultValue: defaultStrategy, +}); + +export const createRollingStrategy = ( + config: RollingStrategyConfig, + context: RollingFileContext +): RollingStrategy => { + return new NumericRollingStrategy(config, context); +}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/services/index.js b/src/core/server/logging/appenders/rolling_file/strategies/numeric/index.ts similarity index 85% rename from src/plugins/es_ui_shared/public/components/cron_editor/services/index.js rename to src/core/server/logging/appenders/rolling_file/strategies/numeric/index.ts index cb4af15bf1945..f5b6ae740b155 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/services/index.js +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/index.ts @@ -17,5 +17,8 @@ * under the License. */ -export * from './cron'; -export * from './humanized_numbers'; +export { + NumericRollingStrategy, + NumericRollingStrategyConfig, + numericRollingStrategyConfigSchema, +} from './numeric_strategy'; diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.mocks.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.mocks.ts new file mode 100644 index 0000000000000..661ca87874e08 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.mocks.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. + */ + +export const getOrderedRolledFilesMock = jest.fn(); +export const deleteFilesMock = jest.fn(); +export const rollPreviousFilesInOrderMock = jest.fn(); +export const rollCurrentFileMock = jest.fn(); +export const shouldSkipRolloutMock = jest.fn(); + +jest.doMock('./rolling_tasks', () => ({ + getOrderedRolledFiles: getOrderedRolledFilesMock, + deleteFiles: deleteFilesMock, + rollPreviousFilesInOrder: rollPreviousFilesInOrderMock, + rollCurrentFile: rollCurrentFileMock, + shouldSkipRollout: shouldSkipRolloutMock, +})); + +export const resetAllMock = () => { + shouldSkipRolloutMock.mockReset(); + getOrderedRolledFilesMock.mockReset(); + deleteFilesMock.mockReset(); + rollPreviousFilesInOrderMock.mockReset(); + rollCurrentFileMock.mockReset(); +}; diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts new file mode 100644 index 0000000000000..386b551aee377 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts @@ -0,0 +1,172 @@ +/* + * 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 { join } from 'path'; +import { + resetAllMock, + shouldSkipRolloutMock, + deleteFilesMock, + getOrderedRolledFilesMock, + rollCurrentFileMock, + rollPreviousFilesInOrderMock, +} from './numeric_strategy.test.mocks'; +import { rollingFileAppenderMocks } from '../../mocks'; +import { NumericRollingStrategy, NumericRollingStrategyConfig } from './numeric_strategy'; + +const logFileFolder = 'log-file-folder'; +const logFileBaseName = 'kibana.log'; +const pattern = '.%i'; +const logFilePath = join(logFileFolder, logFileBaseName); + +describe('NumericRollingStrategy', () => { + let context: ReturnType; + let strategy: NumericRollingStrategy; + + const createStrategy = (config: Omit) => + new NumericRollingStrategy({ ...config, kind: 'numeric' }, context); + + beforeEach(() => { + context = rollingFileAppenderMocks.createContext(logFilePath); + strategy = createStrategy({ pattern, max: 3 }); + shouldSkipRolloutMock.mockResolvedValue(false); + getOrderedRolledFilesMock.mockResolvedValue([]); + }); + + afterEach(() => { + resetAllMock(); + }); + + it('calls `getOrderedRolledFiles` with the correct parameters', async () => { + await strategy.rollout(); + + expect(getOrderedRolledFilesMock).toHaveBeenCalledTimes(1); + expect(getOrderedRolledFilesMock).toHaveBeenCalledWith({ + logFileFolder, + logFileBaseName, + pattern, + }); + }); + + it('calls `deleteFiles` with the correct files', async () => { + getOrderedRolledFilesMock.mockResolvedValue([ + 'kibana.1.log', + 'kibana.2.log', + 'kibana.3.log', + 'kibana.4.log', + ]); + + await strategy.rollout(); + + expect(deleteFilesMock).toHaveBeenCalledTimes(1); + expect(deleteFilesMock).toHaveBeenCalledWith({ + filesToDelete: ['kibana.3.log', 'kibana.4.log'], + logFileFolder, + }); + }); + + it('calls `rollPreviousFilesInOrder` with the correct files', async () => { + getOrderedRolledFilesMock.mockResolvedValue([ + 'kibana.1.log', + 'kibana.2.log', + 'kibana.3.log', + 'kibana.4.log', + ]); + + await strategy.rollout(); + + expect(rollPreviousFilesInOrderMock).toHaveBeenCalledTimes(1); + expect(rollPreviousFilesInOrderMock).toHaveBeenCalledWith({ + filesToRoll: ['kibana.1.log', 'kibana.2.log'], + logFileFolder, + logFileBaseName, + pattern, + }); + }); + + it('calls `rollCurrentFile` with the correct parameters', async () => { + await strategy.rollout(); + + expect(rollCurrentFileMock).toHaveBeenCalledTimes(1); + expect(rollCurrentFileMock).toHaveBeenCalledWith({ + pattern, + logFileBaseName, + logFileFolder, + }); + }); + + it('calls `context.refreshFileInfo` with the correct parameters', async () => { + await strategy.rollout(); + + expect(context.refreshFileInfo).toHaveBeenCalledTimes(1); + }); + + it('calls the tasks in the correct order', async () => { + getOrderedRolledFilesMock.mockResolvedValue([ + 'kibana.1.log', + 'kibana.2.log', + 'kibana.3.log', + 'kibana.4.log', + ]); + + await strategy.rollout(); + + const deleteFilesCall = deleteFilesMock.mock.invocationCallOrder[0]; + const rollPreviousFilesInOrderCall = rollPreviousFilesInOrderMock.mock.invocationCallOrder[0]; + const rollCurrentFileCall = rollCurrentFileMock.mock.invocationCallOrder[0]; + const refreshFileInfoCall = context.refreshFileInfo.mock.invocationCallOrder[0]; + + expect(deleteFilesCall).toBeLessThan(rollPreviousFilesInOrderCall); + expect(rollPreviousFilesInOrderCall).toBeLessThan(rollCurrentFileCall); + expect(rollCurrentFileCall).toBeLessThan(refreshFileInfoCall); + }); + + it('do not calls `deleteFiles` if no file should be deleted', async () => { + getOrderedRolledFilesMock.mockResolvedValue(['kibana.1.log', 'kibana.2.log']); + + await strategy.rollout(); + + expect(deleteFilesMock).not.toHaveBeenCalled(); + }); + + it('do not calls `rollPreviousFilesInOrder` if no file should be rolled', async () => { + getOrderedRolledFilesMock.mockResolvedValue([]); + + await strategy.rollout(); + + expect(rollPreviousFilesInOrderMock).not.toHaveBeenCalled(); + }); + + it('skips the rollout if `shouldSkipRollout` returns true', async () => { + shouldSkipRolloutMock.mockResolvedValue(true); + getOrderedRolledFilesMock.mockResolvedValue([ + 'kibana.1.log', + 'kibana.2.log', + 'kibana.3.log', + 'kibana.4.log', + ]); + + await strategy.rollout(); + + expect(getOrderedRolledFilesMock).not.toHaveBeenCalled(); + expect(deleteFilesMock).not.toHaveBeenCalled(); + expect(rollPreviousFilesInOrderMock).not.toHaveBeenCalled(); + expect(rollCurrentFileMock).not.toHaveBeenCalled(); + expect(context.refreshFileInfo).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts new file mode 100644 index 0000000000000..009f34f4a6203 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts @@ -0,0 +1,152 @@ +/* + * 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 { basename, dirname } from 'path'; +import { schema } from '@kbn/config-schema'; +import { RollingStrategy } from '../strategy'; +import { RollingFileContext } from '../../rolling_file_context'; +import { + shouldSkipRollout, + getOrderedRolledFiles, + deleteFiles, + rollCurrentFile, + rollPreviousFilesInOrder, +} from './rolling_tasks'; + +export interface NumericRollingStrategyConfig { + kind: 'numeric'; + /** + * The suffix pattern to apply when renaming a file. The suffix will be applied + * after the `appender.path` file name, but before the file extension. + * + * Must include `%i`, as it is the value that will be converted to the file index + * + * @example + * ```yaml + * logging: + * appenders: + * rolling-file: + * kind: rolling-file + * path: /var/logs/kibana.log + * strategy: + * type: default + * pattern: "-%i" + * max: 5 + * ``` + * + * will create `/var/logs/kibana-1.log`, `/var/logs/kibana-2.log`, and so on. + * + * Defaults to `-%i`. + */ + pattern: string; + /** + * The maximum number of files to keep. Once this number is reached, oldest + * files will be deleted. Defaults to `7` + */ + max: number; +} + +export const numericRollingStrategyConfigSchema = schema.object({ + kind: schema.literal('numeric'), + pattern: schema.string({ + defaultValue: '-%i', + validate: (pattern) => { + if (!pattern.includes('%i')) { + return `pattern must include '%i'`; + } + }, + }), + max: schema.number({ min: 1, max: 100, defaultValue: 7 }), +}); + +/** + * A rolling strategy that will suffix the file with a given pattern when rolling, + * and will only retain a fixed amount of rolled files. + * + * @example + * ```yaml + * logging: + * appenders: + * rolling-file: + * kind: rolling-file + * path: /kibana.log + * strategy: + * type: numeric + * pattern: "-%i" + * max: 2 + * ``` + * - During the first rollover kibana.log is renamed to kibana-1.log. A new kibana.log file is created and starts + * being written to. + * - During the second rollover kibana-1.log is renamed to kibana-2.log and kibana.log is renamed to kibana-1.log. + * A new kibana.log file is created and starts being written to. + * - During the third and subsequent rollovers, kibana-2.log is deleted, kibana-1.log is renamed to kibana-2.log and + * kibana.log is renamed to kibana-1.log. A new kibana.log file is created and starts being written to. + * + * See {@link NumericRollingStrategyConfig} for more details. + */ +export class NumericRollingStrategy implements RollingStrategy { + private readonly logFilePath; + private readonly logFileBaseName; + private readonly logFileFolder; + + constructor( + private readonly config: NumericRollingStrategyConfig, + private readonly context: RollingFileContext + ) { + this.logFilePath = this.context.filePath; + this.logFileBaseName = basename(this.context.filePath); + this.logFileFolder = dirname(this.context.filePath); + } + + async rollout() { + const logFilePath = this.logFilePath; + const logFileBaseName = this.logFileBaseName; + const logFileFolder = this.logFileFolder; + const pattern = this.config.pattern; + + if (await shouldSkipRollout({ logFilePath })) { + return; + } + + // get the files matching the pattern in the folder, and sort them by `%i` value + const orderedFiles = await getOrderedRolledFiles({ + logFileFolder, + logFileBaseName, + pattern, + }); + const filesToRoll = orderedFiles.slice(0, this.config.max - 1); + const filesToDelete = orderedFiles.slice(filesToRoll.length, orderedFiles.length); + + if (filesToDelete.length > 0) { + await deleteFiles({ logFileFolder, filesToDelete }); + } + + if (filesToRoll.length > 0) { + await rollPreviousFilesInOrder({ filesToRoll, logFileFolder, logFileBaseName, pattern }); + } + + await rollCurrentFile({ pattern, logFileBaseName, logFileFolder }); + + // updates the context file info to mirror the new size and date + // this is required for the time based policy, as the next time check + // will be performed before the file manager updates the context itself by reopening + // a writer to the new file. + this.context.refreshFileInfo(); + } +} diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/pattern_matcher.test.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/pattern_matcher.test.ts new file mode 100644 index 0000000000000..8f29ff3346130 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/pattern_matcher.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { getFileNameMatcher, getRollingFileName } from './pattern_matcher'; + +describe('getFileNameMatcher', () => { + it('returns the file index when the file matches the pattern', () => { + const matcher = getFileNameMatcher('log.json', '.%i'); + expect(matcher('log.1.json')).toEqual(1); + expect(matcher('log.12.json')).toEqual(12); + }); + it('handles special characters in the pattern', () => { + const matcher = getFileNameMatcher('kibana.log', '-{%i}'); + expect(matcher('kibana-{1}.log')).toEqual(1); + }); + it('returns undefined when the file does not match the pattern', () => { + const matcher = getFileNameMatcher('log.json', '.%i'); + expect(matcher('log.1.text')).toBeUndefined(); + expect(matcher('log*1.json')).toBeUndefined(); + expect(matcher('log.2foo.json')).toBeUndefined(); + }); + it('handles multiple extensions', () => { + const matcher = getFileNameMatcher('log.foo.bar', '.%i'); + expect(matcher('log.1.foo.bar')).toEqual(1); + expect(matcher('log.12.foo.bar')).toEqual(12); + }); + it('handles files without extension', () => { + const matcher = getFileNameMatcher('log', '.%i'); + expect(matcher('log.1')).toEqual(1); + expect(matcher('log.42')).toEqual(42); + }); +}); + +describe('getRollingFileName', () => { + it('returns the correct file name', () => { + expect(getRollingFileName('kibana.json', '.%i', 5)).toEqual('kibana.5.json'); + expect(getRollingFileName('log.txt', '-%i', 3)).toEqual('log-3.txt'); + }); + + it('handles multiple extensions', () => { + expect(getRollingFileName('kibana.foo.bar', '.%i', 5)).toEqual('kibana.5.foo.bar'); + expect(getRollingFileName('log.foo.bar', '-%i', 3)).toEqual('log-3.foo.bar'); + }); + + it('handles files without extension', () => { + expect(getRollingFileName('kibana', '.%i', 12)).toEqual('kibana.12'); + expect(getRollingFileName('log', '-%i', 7)).toEqual('log-7'); + }); +}); diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/pattern_matcher.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/pattern_matcher.ts new file mode 100644 index 0000000000000..91004cca94e26 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/pattern_matcher.ts @@ -0,0 +1,81 @@ +/* + * 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 { escapeRegExp } from 'lodash'; + +const createNumericMatcher = (fileBaseName: string, pattern: string): RegExp => { + let extStart = fileBaseName.indexOf('.'); + if (extStart === -1) { + extStart = fileBaseName.length; + } + const baseNameWithoutExt = escapeRegExp(fileBaseName.substr(0, extStart)); + const extension = escapeRegExp(fileBaseName.substr(extStart, fileBaseName.length)); + const processedPattern = escapeRegExp(pattern) + // create matching group for `%i` + .replace(/%i/g, '(?\\d+)'); + return new RegExp(`^${baseNameWithoutExt}${processedPattern}${extension}$`); +}; + +/** + * Builds a matcher that can be used to match a filename against the rolling + * file name pattern associated with given `logFileName` and `pattern` + * + * @example + * ```ts + * const matcher = getFileNameMatcher('kibana.log', '-%i'); + * matcher('kibana-1.log') // `1` + * matcher('kibana-5.log') // `5` + * matcher('kibana-A.log') // undefined + * matcher('kibana.log') // undefined + * ``` + */ +export const getFileNameMatcher = (logFileName: string, pattern: string) => { + const matcher = createNumericMatcher(logFileName, pattern); + return (fileName: string): number | undefined => { + const match = matcher.exec(fileName); + if (!match) { + return undefined; + } + return parseInt(match.groups!.counter, 10); + }; +}; + +/** + * Returns the rolling file name associated with given basename and pattern for given index. + * + * @example + * ```ts + * getNumericFileName('foo.log', '.%i', 4) // -> `foo.4.log` + * getNumericFileName('kibana.log', '-{%i}', 12) // -> `kibana-{12}.log` + * ``` + */ +export const getRollingFileName = ( + fileBaseName: string, + pattern: string, + index: number +): string => { + let suffixStart = fileBaseName.indexOf('.'); + if (suffixStart === -1) { + suffixStart = fileBaseName.length; + } + const baseNameWithoutSuffix = fileBaseName.substr(0, suffixStart); + const suffix = fileBaseName.substr(suffixStart, fileBaseName.length); + const interpolatedPattern = pattern.replace('%i', String(index)); + return `${baseNameWithoutSuffix}${interpolatedPattern}${suffix}`; +}; diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/rolling_tasks.test.mocks.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/rolling_tasks.test.mocks.ts new file mode 100644 index 0000000000000..4355ec7ffb2ec --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/rolling_tasks.test.mocks.ts @@ -0,0 +1,37 @@ +/* + * 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 readdirMock = jest.fn(); +export const unlinkMock = jest.fn(); +export const renameMock = jest.fn(); +export const accessMock = jest.fn(); + +jest.doMock('fs/promises', () => ({ + readdir: readdirMock, + unlink: unlinkMock, + rename: renameMock, + access: accessMock, +})); + +export const clearAllMocks = () => { + readdirMock.mockClear(); + unlinkMock.mockClear(); + renameMock.mockClear(); + accessMock.mockClear(); +}; diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/rolling_tasks.test.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/rolling_tasks.test.ts new file mode 100644 index 0000000000000..469ea450485a1 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/rolling_tasks.test.ts @@ -0,0 +1,173 @@ +/* + * 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 { join } from 'path'; +import { + accessMock, + readdirMock, + renameMock, + unlinkMock, + clearAllMocks, +} from './rolling_tasks.test.mocks'; +import { + shouldSkipRollout, + rollCurrentFile, + rollPreviousFilesInOrder, + deleteFiles, + getOrderedRolledFiles, +} from './rolling_tasks'; + +describe('NumericRollingStrategy tasks', () => { + afterEach(() => { + clearAllMocks(); + }); + + describe('shouldSkipRollout', () => { + it('calls `exists` with the correct parameters', async () => { + await shouldSkipRollout({ logFilePath: 'some-file' }); + + expect(accessMock).toHaveBeenCalledTimes(1); + expect(accessMock).toHaveBeenCalledWith('some-file'); + }); + it('returns `true` if the file is current log file does not exist', async () => { + accessMock.mockImplementation(() => { + throw new Error('ENOENT'); + }); + + expect(await shouldSkipRollout({ logFilePath: 'some-file' })).toEqual(true); + }); + it('returns `false` if the file is current log file exists', async () => { + accessMock.mockResolvedValue(undefined); + + expect(await shouldSkipRollout({ logFilePath: 'some-file' })).toEqual(false); + }); + }); + + describe('rollCurrentFile', () => { + it('calls `rename` with the correct parameters', async () => { + await rollCurrentFile({ + logFileFolder: 'log-folder', + logFileBaseName: 'kibana.log', + pattern: '.%i', + }); + + expect(renameMock).toHaveBeenCalledTimes(1); + expect(renameMock).toHaveBeenCalledWith( + join('log-folder', 'kibana.log'), + join('log-folder', 'kibana.1.log') + ); + }); + }); + + describe('rollPreviousFilesInOrder', () => { + it('calls `rename` once for each file', async () => { + await rollPreviousFilesInOrder({ + filesToRoll: ['file-1', 'file-2', 'file-3'], + logFileFolder: 'log-folder', + logFileBaseName: 'file', + pattern: '-%i', + }); + + expect(renameMock).toHaveBeenCalledTimes(3); + }); + + it('calls `rename` with the correct parameters', async () => { + await rollPreviousFilesInOrder({ + filesToRoll: ['file-1', 'file-2'], + logFileFolder: 'log-folder', + logFileBaseName: 'file', + pattern: '-%i', + }); + + expect(renameMock).toHaveBeenNthCalledWith( + 1, + join('log-folder', 'file-2'), + join('log-folder', 'file-3') + ); + expect(renameMock).toHaveBeenNthCalledWith( + 2, + join('log-folder', 'file-1'), + join('log-folder', 'file-2') + ); + }); + }); + + describe('deleteFiles', () => { + it('calls `unlink` once for each file', async () => { + await deleteFiles({ + logFileFolder: 'log-folder', + filesToDelete: ['file-a', 'file-b', 'file-c'], + }); + + expect(unlinkMock).toHaveBeenCalledTimes(3); + }); + it('calls `unlink` with the correct parameters', async () => { + await deleteFiles({ + logFileFolder: 'log-folder', + filesToDelete: ['file-a', 'file-b'], + }); + + expect(unlinkMock).toHaveBeenNthCalledWith(1, join('log-folder', 'file-a')); + expect(unlinkMock).toHaveBeenNthCalledWith(2, join('log-folder', 'file-b')); + }); + }); + + describe('getOrderedRolledFiles', () => { + it('returns the rolled files matching the pattern in order', async () => { + readdirMock.mockResolvedValue([ + 'kibana-10.log', + 'kibana-1.log', + 'kibana-12.log', + 'kibana-2.log', + ]); + + const files = await getOrderedRolledFiles({ + logFileFolder: 'log-folder', + logFileBaseName: 'kibana.log', + pattern: '-%i', + }); + + expect(files).toEqual(['kibana-1.log', 'kibana-2.log', 'kibana-10.log', 'kibana-12.log']); + }); + + it('ignores files that do no match the pattern', async () => { + readdirMock.mockResolvedValue(['kibana.2.log', 'kibana.1.log', 'kibana-3.log', 'foo.log']); + + const files = await getOrderedRolledFiles({ + logFileFolder: 'log-folder', + logFileBaseName: 'kibana.log', + pattern: '.%i', + }); + + expect(files).toEqual(['kibana.1.log', 'kibana.2.log']); + }); + + it('does not return the base log file', async () => { + readdirMock.mockResolvedValue(['kibana.log', 'kibana-1.log', 'kibana-2.log']); + + const files = await getOrderedRolledFiles({ + logFileFolder: 'log-folder', + logFileBaseName: 'kibana.log', + pattern: '-%i', + }); + + expect(files).toEqual(['kibana-1.log', 'kibana-2.log']); + }); + }); +}); diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/rolling_tasks.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/rolling_tasks.ts new file mode 100644 index 0000000000000..6fe065c5c1561 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/rolling_tasks.ts @@ -0,0 +1,99 @@ +/* + * 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 { join } from 'path'; +import { readdir, rename, unlink, access } from 'fs/promises'; +import { getFileNameMatcher, getRollingFileName } from './pattern_matcher'; + +export const shouldSkipRollout = async ({ logFilePath }: { logFilePath: string }) => { + // in case of time-interval triggering policy, we can have an entire + // interval without any log event. In that case, the log file is not even + // present, and we should not perform the rollout + try { + await access(logFilePath); + return false; + } catch (e) { + return true; + } +}; + +/** + * Returns the rolled file basenames, from the most recent to the oldest. + */ +export const getOrderedRolledFiles = async ({ + logFileBaseName, + logFileFolder, + pattern, +}: { + logFileFolder: string; + logFileBaseName: string; + pattern: string; +}): Promise => { + const matcher = getFileNameMatcher(logFileBaseName, pattern); + const dirContent = await readdir(logFileFolder); + return dirContent + .map((fileName) => ({ + fileName, + index: matcher(fileName), + })) + .filter(({ index }) => index !== undefined) + .sort((a, b) => a.index! - b.index!) + .map(({ fileName }) => fileName); +}; + +export const deleteFiles = async ({ + logFileFolder, + filesToDelete, +}: { + logFileFolder: string; + filesToDelete: string[]; +}) => { + await Promise.all(filesToDelete.map((fileToDelete) => unlink(join(logFileFolder, fileToDelete)))); +}; + +export const rollPreviousFilesInOrder = async ({ + filesToRoll, + logFileFolder, + logFileBaseName, + pattern, +}: { + logFileFolder: string; + logFileBaseName: string; + pattern: string; + filesToRoll: string[]; +}) => { + for (let i = filesToRoll.length - 1; i >= 0; i--) { + const oldFileName = filesToRoll[i]; + const newFileName = getRollingFileName(logFileBaseName, pattern, i + 2); + await rename(join(logFileFolder, oldFileName), join(logFileFolder, newFileName)); + } +}; + +export const rollCurrentFile = async ({ + logFileFolder, + logFileBaseName, + pattern, +}: { + logFileFolder: string; + logFileBaseName: string; + pattern: string; +}) => { + const rolledBaseName = getRollingFileName(logFileBaseName, pattern, 1); + await rename(join(logFileFolder, logFileBaseName), join(logFileFolder, rolledBaseName)); +}; diff --git a/src/core/server/logging/appenders/rolling_file/strategies/strategy.ts b/src/core/server/logging/appenders/rolling_file/strategies/strategy.ts new file mode 100644 index 0000000000000..fb5984dfb5df3 --- /dev/null +++ b/src/core/server/logging/appenders/rolling_file/strategies/strategy.ts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +/** + * A strategy to perform the log file rollover. + */ +export interface RollingStrategy { + /** + * Performs the rollout + */ + rollout(): Promise; +} diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index 7f6059567c46e..bf9934b64a419 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -146,12 +146,18 @@ describe('logging service', () => { ], }; + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + let root: ReturnType; let setup: InternalCoreSetup; let mockConsoleLog: jest.SpyInstance; const loggingConfig$ = new Subject(); - const setContextConfig = (enable: boolean) => - enable ? loggingConfig$.next(CUSTOM_LOGGING_CONFIG) : loggingConfig$.next({}); + const setContextConfig = async (enable: boolean) => { + loggingConfig$.next(enable ? CUSTOM_LOGGING_CONFIG : {}); + // need to wait for config to reload. nextTick is enough, using delay just to be sure + await delay(10); + }; + beforeAll(async () => { mockConsoleLog = jest.spyOn(global.console, 'log'); root = kbnTestServer.createRoot(); @@ -171,12 +177,12 @@ describe('logging service', () => { it('does not write to custom appenders when not configured', async () => { const logger = root.logger.get('plugins.myplugin.debug_pattern'); - setContextConfig(false); + await setContextConfig(false); logger.info('log1'); - setContextConfig(true); + await setContextConfig(true); logger.debug('log2'); logger.info('log3'); - setContextConfig(false); + await setContextConfig(false); logger.info('log4'); expect(mockConsoleLog).toHaveBeenCalledTimes(2); expect(mockConsoleLog).toHaveBeenCalledWith( @@ -188,7 +194,7 @@ describe('logging service', () => { }); it('writes debug_json context to custom JSON appender', async () => { - setContextConfig(true); + await setContextConfig(true); const logger = root.logger.get('plugins.myplugin.debug_json'); logger.debug('log1'); logger.info('log2'); @@ -214,7 +220,7 @@ describe('logging service', () => { }); it('writes info_json context to custom JSON appender', async () => { - setContextConfig(true); + await setContextConfig(true); const logger = root.logger.get('plugins.myplugin.info_json'); logger.debug('i should not be logged!'); logger.info('log2'); @@ -230,7 +236,7 @@ describe('logging service', () => { }); it('writes debug_pattern context to custom pattern appender', async () => { - setContextConfig(true); + await setContextConfig(true); const logger = root.logger.get('plugins.myplugin.debug_pattern'); logger.debug('log1'); logger.info('log2'); @@ -245,7 +251,7 @@ describe('logging service', () => { }); it('writes info_pattern context to custom pattern appender', async () => { - setContextConfig(true); + await setContextConfig(true); const logger = root.logger.get('plugins.myplugin.info_pattern'); logger.debug('i should not be logged!'); logger.info('log2'); @@ -256,7 +262,7 @@ describe('logging service', () => { }); it('writes all context to both appenders', async () => { - setContextConfig(true); + await setContextConfig(true); const logger = root.logger.get('plugins.myplugin.all'); logger.debug('log1'); logger.info('log2'); diff --git a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts new file mode 100644 index 0000000000000..4680740195b44 --- /dev/null +++ b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts @@ -0,0 +1,220 @@ +/* + * 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 { join } from 'path'; +import { rmdir, mkdtemp, readFile, readdir } from 'fs/promises'; +import moment from 'moment-timezone'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; +import { getNextRollingTime } from '../appenders/rolling_file/policies/time_interval/get_next_rolling_time'; + +const flushDelay = 250; +const delay = (waitInMs: number) => new Promise((resolve) => setTimeout(resolve, waitInMs)); +const flush = async () => delay(flushDelay); + +function createRoot(appenderConfig: any) { + return kbnTestServer.createRoot({ + logging: { + silent: true, // set "true" in kbnTestServer + appenders: { + 'rolling-file': appenderConfig, + }, + loggers: [ + { + context: 'test.rolling.file', + appenders: ['rolling-file'], + level: 'debug', + }, + ], + }, + }); +} + +describe('RollingFileAppender', () => { + let root: ReturnType; + let testDir: string; + let logFile: string; + + const getFileContent = async (basename: string) => + (await readFile(join(testDir, basename))).toString('utf-8'); + + beforeEach(async () => { + testDir = await mkdtemp('rolling-test'); + logFile = join(testDir, 'kibana.log'); + }); + + afterEach(async () => { + try { + await rmdir(testDir); + } catch (e) { + /* trap */ + } + if (root) { + await root.shutdown(); + } + }); + + const message = (index: number) => `some message of around 40 bytes number ${index}`; + const expectedFileContent = (indices: number[]) => indices.map(message).join('\n') + '\n'; + + describe('`size-limit` policy with `numeric` strategy', () => { + it('rolls the log file in the correct order', async () => { + root = createRoot({ + kind: 'rolling-file', + path: logFile, + layout: { + kind: 'pattern', + pattern: '%message', + }, + policy: { + kind: 'size-limit', + size: '100b', + }, + strategy: { + kind: 'numeric', + max: 5, + pattern: '.%i', + }, + }); + await root.setup(); + + const logger = root.logger.get('test.rolling.file'); + + // size = 100b, message.length ~= 40b, should roll every 3 message + + // last file - 'kibana.2.log' + logger.info(message(1)); + logger.info(message(2)); + logger.info(message(3)); + // roll - 'kibana.1.log' + logger.info(message(4)); + logger.info(message(5)); + logger.info(message(6)); + // roll - 'kibana.log' + logger.info(message(7)); + + await flush(); + + const files = await readdir(testDir); + + expect(files.sort()).toEqual(['kibana.1.log', 'kibana.2.log', 'kibana.log']); + expect(await getFileContent('kibana.log')).toEqual(expectedFileContent([7])); + expect(await getFileContent('kibana.1.log')).toEqual(expectedFileContent([4, 5, 6])); + expect(await getFileContent('kibana.2.log')).toEqual(expectedFileContent([1, 2, 3])); + }); + + it('only keep the correct number of files', async () => { + root = createRoot({ + kind: 'rolling-file', + path: logFile, + layout: { + kind: 'pattern', + pattern: '%message', + }, + policy: { + kind: 'size-limit', + size: '60b', + }, + strategy: { + kind: 'numeric', + max: 2, + pattern: '-%i', + }, + }); + await root.setup(); + + const logger = root.logger.get('test.rolling.file'); + + // size = 60b, message.length ~= 40b, should roll every 2 message + + // last file - 'kibana-3.log' (which will be removed during the last rolling) + logger.info(message(1)); + logger.info(message(2)); + // roll - 'kibana-2.log' + logger.info(message(3)); + logger.info(message(4)); + // roll - 'kibana-1.log' + logger.info(message(5)); + logger.info(message(6)); + // roll - 'kibana.log' + logger.info(message(7)); + logger.info(message(8)); + + await flush(); + + const files = await readdir(testDir); + + expect(files.sort()).toEqual(['kibana-1.log', 'kibana-2.log', 'kibana.log']); + expect(await getFileContent('kibana.log')).toEqual(expectedFileContent([7, 8])); + expect(await getFileContent('kibana-1.log')).toEqual(expectedFileContent([5, 6])); + expect(await getFileContent('kibana-2.log')).toEqual(expectedFileContent([3, 4])); + }); + }); + + describe('`time-interval` policy with `numeric` strategy', () => { + it('rolls the log file at the given interval', async () => { + root = createRoot({ + kind: 'rolling-file', + path: logFile, + layout: { + kind: 'pattern', + pattern: '%message', + }, + policy: { + kind: 'time-interval', + interval: '1s', + modulate: true, + }, + strategy: { + kind: 'numeric', + max: 2, + pattern: '-%i', + }, + }); + await root.setup(); + + const logger = root.logger.get('test.rolling.file'); + + const waitForNextRollingTime = () => { + const now = Date.now(); + const nextRolling = getNextRollingTime(now, moment.duration(1, 'second'), true); + return delay(nextRolling - now + 1); + }; + + // wait for a rolling time boundary to minimize the risk to have logs emitted in different intervals + // the `1s` interval should be way more than enough to log 2 messages + await waitForNextRollingTime(); + + logger.info(message(1)); + logger.info(message(2)); + + await waitForNextRollingTime(); + + logger.info(message(3)); + logger.info(message(4)); + + await flush(); + + const files = await readdir(testDir); + + expect(files.sort()).toEqual(['kibana-1.log', 'kibana.log']); + expect(await getFileContent('kibana.log')).toEqual(expectedFileContent([3, 4])); + expect(await getFileContent('kibana-1.log')).toEqual(expectedFileContent([1, 2])); + }); + }); +}); diff --git a/src/core/server/logging/logging_system.mock.ts b/src/core/server/logging/logging_system.mock.ts index 6ea784be5411f..35d7caf0914e7 100644 --- a/src/core/server/logging/logging_system.mock.ts +++ b/src/core/server/logging/logging_system.mock.ts @@ -42,6 +42,7 @@ const createLoggingSystemMock = () => { context, })); mocked.asLoggerFactory.mockImplementation(() => mocked); + mocked.upgrade.mockResolvedValue(undefined); mocked.stop.mockResolvedValue(); return mocked; }; diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts index 2fca2f35cb032..171a88f28e128 100644 --- a/src/core/server/logging/logging_system.test.ts +++ b/src/core/server/logging/logging_system.test.ts @@ -19,6 +19,7 @@ const mockStreamWrite = jest.fn(); jest.mock('fs', () => ({ + ...(jest.requireActual('fs') as any), constants: {}, createWriteStream: jest.fn(() => ({ write: mockStreamWrite })), })); @@ -67,7 +68,7 @@ test('uses default memory buffer logger until config is provided', () => { expect(bufferAppendSpy.mock.calls[1][0]).toMatchSnapshot({ pid: expect.any(Number) }); }); -test('flushes memory buffer logger and switches to real logger once config is provided', () => { +test('flushes memory buffer logger and switches to real logger once config is provided', async () => { const logger = system.get('test', 'context'); logger.trace('buffered trace message'); @@ -77,7 +78,7 @@ test('flushes memory buffer logger and switches to real logger once config is pr const bufferAppendSpy = jest.spyOn((system as any).bufferAppender, 'append'); // Switch to console appender with `info` level, so that `trace` message won't go through. - system.upgrade( + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, root: { level: 'info' }, @@ -96,7 +97,7 @@ test('flushes memory buffer logger and switches to real logger once config is pr expect(bufferAppendSpy).not.toHaveBeenCalled(); }); -test('appends records via multiple appenders.', () => { +test('appends records via multiple appenders.', async () => { const loggerWithoutConfig = system.get('some-context'); const testsLogger = system.get('tests'); const testsChildLogger = system.get('tests', 'child'); @@ -109,7 +110,7 @@ test('appends records via multiple appenders.', () => { expect(mockConsoleLog).not.toHaveBeenCalled(); expect(mockCreateWriteStream).not.toHaveBeenCalled(); - system.upgrade( + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'pattern' } }, @@ -131,8 +132,8 @@ test('appends records via multiple appenders.', () => { expect(mockStreamWrite.mock.calls[1][0]).toMatchSnapshot('file logs'); }); -test('uses `root` logger if context is not specified.', () => { - system.upgrade( +test('uses `root` logger if context is not specified.', async () => { + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'pattern' } } }, }) @@ -145,7 +146,7 @@ test('uses `root` logger if context is not specified.', () => { }); test('`stop()` disposes all appenders.', async () => { - system.upgrade( + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, root: { level: 'info' }, @@ -161,10 +162,10 @@ test('`stop()` disposes all appenders.', async () => { expect(consoleDisposeSpy).toHaveBeenCalledTimes(1); }); -test('asLoggerFactory() only allows to create new loggers.', () => { +test('asLoggerFactory() only allows to create new loggers.', async () => { const logger = system.asLoggerFactory().get('test', 'context'); - system.upgrade( + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, root: { level: 'all' }, @@ -183,19 +184,19 @@ test('asLoggerFactory() only allows to create new loggers.', () => { expect(JSON.parse(mockConsoleLog.mock.calls[2][0])).toMatchSnapshot(dynamicProps); }); -test('setContextConfig() updates config with relative contexts', () => { +test('setContextConfig() updates config with relative contexts', async () => { const testsLogger = system.get('tests'); const testsChildLogger = system.get('tests', 'child'); const testsGrandchildLogger = system.get('tests', 'child', 'grandchild'); - system.upgrade( + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, root: { level: 'info' }, }) ); - system.setContextConfig(['tests', 'child'], { + await system.setContextConfig(['tests', 'child'], { appenders: new Map([ [ 'custom', @@ -238,19 +239,19 @@ test('setContextConfig() updates config with relative contexts', () => { ); }); -test('setContextConfig() updates config for a root context', () => { +test('setContextConfig() updates config for a root context', async () => { const testsLogger = system.get('tests'); const testsChildLogger = system.get('tests', 'child'); const testsGrandchildLogger = system.get('tests', 'child', 'grandchild'); - system.upgrade( + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, root: { level: 'info' }, }) ); - system.setContextConfig(['tests', 'child'], { + await system.setContextConfig(['tests', 'child'], { appenders: new Map([ [ 'custom', @@ -283,8 +284,8 @@ test('setContextConfig() updates config for a root context', () => { ); }); -test('custom context configs are applied on subsequent calls to update()', () => { - system.setContextConfig(['tests', 'child'], { +test('custom context configs are applied on subsequent calls to update()', async () => { + await system.setContextConfig(['tests', 'child'], { appenders: new Map([ [ 'custom', @@ -295,7 +296,7 @@ test('custom context configs are applied on subsequent calls to update()', () => }); // Calling upgrade after setContextConfig should not throw away the context-specific config - system.upgrade( + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, root: { level: 'info' }, @@ -320,15 +321,15 @@ test('custom context configs are applied on subsequent calls to update()', () => ); }); -test('subsequent calls to setContextConfig() for the same context override the previous config', () => { - system.upgrade( +test('subsequent calls to setContextConfig() for the same context override the previous config', async () => { + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, root: { level: 'info' }, }) ); - system.setContextConfig(['tests', 'child'], { + await system.setContextConfig(['tests', 'child'], { appenders: new Map([ [ 'custom', @@ -339,7 +340,7 @@ test('subsequent calls to setContextConfig() for the same context override the p }); // Call again, this time with level: 'warn' and a different pattern - system.setContextConfig(['tests', 'child'], { + await system.setContextConfig(['tests', 'child'], { appenders: new Map([ [ 'custom', @@ -370,15 +371,15 @@ test('subsequent calls to setContextConfig() for the same context override the p ); }); -test('subsequent calls to setContextConfig() for the same context can disable the previous config', () => { - system.upgrade( +test('subsequent calls to setContextConfig() for the same context can disable the previous config', async () => { + await system.upgrade( config.schema.validate({ appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, root: { level: 'info' }, }) ); - system.setContextConfig(['tests', 'child'], { + await system.setContextConfig(['tests', 'child'], { appenders: new Map([ [ 'custom', @@ -389,7 +390,7 @@ test('subsequent calls to setContextConfig() for the same context can disable th }); // Call again, this time no customizations (effectively disabling) - system.setContextConfig(['tests', 'child'], {}); + await system.setContextConfig(['tests', 'child'], {}); const logger = system.get('tests', 'child', 'grandchild'); logger.debug('this should not show anywhere!'); diff --git a/src/core/server/logging/logging_system.ts b/src/core/server/logging/logging_system.ts index 8bc22bdf537af..d9e7eb70dc4ff 100644 --- a/src/core/server/logging/logging_system.ts +++ b/src/core/server/logging/logging_system.ts @@ -30,6 +30,7 @@ import { LoggerContextConfigType, LoggerContextConfigInput, loggerContextConfigSchema, + config as loggingConfig, } from './logging_config'; export type ILoggingSystem = PublicMethodsOf; @@ -48,6 +49,8 @@ export class LoggingSystem implements LoggerFactory { private readonly loggers: Map = new Map(); private readonly contextConfigs = new Map(); + constructor() {} + public get(...contextParts: string[]): Logger { const context = LoggingConfig.getLoggerContext(contextParts); if (!this.loggers.has(context)) { @@ -65,11 +68,13 @@ export class LoggingSystem implements LoggerFactory { /** * Updates all current active loggers with the new config values. - * @param rawConfig New config instance. + * @param rawConfig New config instance. if unspecified, the default logging configuration + * will be used. */ - public upgrade(rawConfig: LoggingConfigType) { - const config = new LoggingConfig(rawConfig)!; - this.applyBaseConfig(config); + public async upgrade(rawConfig?: LoggingConfigType) { + const usedConfig = rawConfig ?? loggingConfig.schema.validate({}); + const config = new LoggingConfig(usedConfig); + await this.applyBaseConfig(config); } /** @@ -93,7 +98,7 @@ export class LoggingSystem implements LoggerFactory { * @param baseContextParts * @param rawConfig */ - public setContextConfig(baseContextParts: string[], rawConfig: LoggerContextConfigInput) { + public async setContextConfig(baseContextParts: string[], rawConfig: LoggerContextConfigInput) { const context = LoggingConfig.getLoggerContext(baseContextParts); const contextConfig = loggerContextConfigSchema.validate(rawConfig); this.contextConfigs.set(context, { @@ -110,7 +115,7 @@ export class LoggingSystem implements LoggerFactory { // If we already have a base config, apply the config. If not, custom context configs // will be picked up on next call to `upgrade`. if (this.baseConfig) { - this.applyBaseConfig(this.baseConfig); + await this.applyBaseConfig(this.baseConfig); } } @@ -154,17 +159,21 @@ export class LoggingSystem implements LoggerFactory { return this.getLoggerConfigByContext(config, LoggingConfig.getParentLoggerContext(context)); } - private applyBaseConfig(newBaseConfig: LoggingConfig) { + private async applyBaseConfig(newBaseConfig: LoggingConfig) { const computedConfig = [...this.contextConfigs.values()].reduce( (baseConfig, contextConfig) => baseConfig.extend(contextConfig), newBaseConfig ); + // reconfigure all the loggers without configuration to have them use the buffer + // appender while we are awaiting for the appenders to be disposed. + for (const [loggerKey, loggerAdapter] of this.loggers) { + loggerAdapter.updateLogger(this.createLogger(loggerKey, undefined)); + } + // Appenders must be reset, so we first dispose of the current ones, then // build up a new set of appenders. - for (const appender of this.appenders.values()) { - appender.dispose(); - } + await Promise.all([...this.appenders.values()].map((a) => a.dispose())); this.appenders.clear(); for (const [appenderKey, appenderConfig] of computedConfig.appenders) { diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index a3f6b27f135be..f6b39ea24262b 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -23,6 +23,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, @@ -67,6 +74,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, @@ -111,6 +125,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, @@ -159,6 +180,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/translations/en.json", }, @@ -203,6 +231,13 @@ Object { "version": Any, }, }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, "i18n": Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 4bbb2bd4811cb..b7c57f1c31e40 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -79,6 +79,7 @@ export class RenderingService { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, }, csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers }, + externalUrl: http.externalUrl, vars: vars ?? {}, uiPlugins: await Promise.all( [...uiPlugins.public].map(async ([id, plugin]) => ({ diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index 1954fc1c79e55..1b73b2be46835 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -25,6 +25,7 @@ import { InternalHttpServiceSetup, KibanaRequest, LegacyRequest } from '../http' import { UiPlugins, DiscoveredPlugin } from '../plugins'; import { IUiSettingsClient, UserProvidedValues } from '../ui_settings'; import type { InternalStatusServiceSetup } from '../status'; +import { IExternalUrlPolicy } from '../external_url'; /** @internal */ export interface RenderingMetadata { @@ -50,6 +51,7 @@ export interface RenderingMetadata { translationsUrl: string; }; csp: Pick; + externalUrl: { policy: IExternalUrlPolicy[] }; vars: Record; uiPlugins: Array<{ id: string; diff --git a/src/core/server/root/index.test.ts b/src/core/server/root/index.test.ts index 4d3fe24c7ba83..1ad0bcde0ca0e 100644 --- a/src/core/server/root/index.test.ts +++ b/src/core/server/root/index.test.ts @@ -33,6 +33,7 @@ let mockConsoleError: jest.SpyInstance; beforeEach(() => { jest.spyOn(global.process, 'exit').mockReturnValue(undefined as never); mockConsoleError = jest.spyOn(console, 'error').mockReturnValue(undefined); + logger.upgrade.mockResolvedValue(undefined); rawConfigService.getConfig$.mockReturnValue(new BehaviorSubject({ someValue: 'foo' })); configService.atPath.mockReturnValue(new BehaviorSubject({ someValue: 'foo' })); }); diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index 5e9722de03dee..1f3aa87498922 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -17,8 +17,8 @@ * under the License. */ -import { ConnectableObservable, Subscription } from 'rxjs'; -import { first, map, publishReplay, switchMap, tap } from 'rxjs/operators'; +import { ConnectableObservable, Subscription, of } from 'rxjs'; +import { first, publishReplay, switchMap, concatMap, tap } from 'rxjs/operators'; import { Env, RawConfigurationProvider } from '../config'; import { Logger, LoggerFactory, LoggingConfigType, LoggingSystem } from '../logging'; @@ -36,7 +36,7 @@ export class Root { constructor( rawConfigProvider: RawConfigurationProvider, - env: Env, + private readonly env: Env, private readonly onShutdown?: (reason?: Error | string) => void ) { this.loggingSystem = new LoggingSystem(); @@ -98,8 +98,11 @@ export class Root { // Stream that maps config updates to logger updates, including update failures. const update$ = configService.getConfig$().pipe( // always read the logging config when the underlying config object is re-read - switchMap(() => configService.atPath('logging')), - map((config) => this.loggingSystem.upgrade(config)), + // except for the CLI process where we only apply the default logging config once + switchMap(() => + this.env.isDevCliParent ? of(undefined) : configService.atPath('logging') + ), + concatMap((config) => this.loggingSystem.upgrade(config)), // This specifically console.logs because we were not able to configure the logger. // eslint-disable-next-line no-console tap({ error: (err) => console.error('Configuring logger failed:', err) }), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index b65ba329cec1e..a39bbecd16ff5 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -7,6 +7,7 @@ import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import Boom from '@hapi/boom'; import { BulkIndexDocumentsParams } from 'elasticsearch'; +import { ByteSizeValue } from '@kbn/config-schema'; import { CatAliasesParams } from 'elasticsearch'; import { CatAllocationParams } from 'elasticsearch'; import { CatCommonParams } from 'elasticsearch'; @@ -47,6 +48,7 @@ import { DeleteScriptParams } from 'elasticsearch'; import { DeleteTemplateParams } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; +import { Duration as Duration_2 } from 'moment-timezone'; import { EnvironmentMode } from '@kbn/config'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; @@ -177,9 +179,10 @@ export interface AppCategory { // Warning: (ae-forgotten-export) The symbol "ConsoleAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FileAppenderConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "LegacyAppenderConfig" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RollingFileAppenderConfig" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig; +export type AppenderConfigType = ConsoleAppenderConfig | FileAppenderConfig | LegacyAppenderConfig | RollingFileAppenderConfig; // @public @deprecated (undocumented) export interface AssistanceAPIResponse { @@ -934,6 +937,18 @@ export interface ICustomClusterClient extends IClusterClient { close: () => Promise; } +// @public +export interface IExternalUrlConfig { + readonly policy: IExternalUrlPolicy[]; +} + +// @public +export interface IExternalUrlPolicy { + allow: boolean; + host?: string; + protocol?: string; +} + // @public export interface IKibanaResponse { // (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 0b3249ad58750..75530e557de04 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -53,6 +53,7 @@ import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; import { CoreUsageDataService } from './core_usage_data'; import { CoreRouteHandlerContext } from './core_route_handler_context'; +import { config as externalUrlConfig } from './external_url'; const coreId = Symbol('core'); const rootConfigPath = ''; @@ -314,6 +315,7 @@ export class Server { pathConfig, cspConfig, elasticsearchConfig, + externalUrlConfig, loggingConfig, httpConfig, pluginsConfig, diff --git a/src/core/server/types.ts b/src/core/server/types.ts index f8d2f635671fa..48b3a9058605c 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -23,3 +23,4 @@ export * from './saved_objects/types'; export * from './ui_settings/types'; export * from './legacy/types'; export type { EnvironmentMode, PackageInfo } from '@kbn/config'; +export type { ExternalUrlConfig, IExternalUrlPolicy } from './external_url'; diff --git a/src/core/server/utils/crypto/index.ts b/src/core/server/utils/crypto/index.ts index 9a36682cc4ecb..aa9728e0462d6 100644 --- a/src/core/server/utils/crypto/index.ts +++ b/src/core/server/utils/crypto/index.ts @@ -18,3 +18,4 @@ */ export { Pkcs12ReadResult, readPkcs12Keystore, readPkcs12Truststore } from './pkcs12'; +export { createSHA256Hash } from './sha256'; diff --git a/tasks/jenkins.js b/src/core/server/utils/crypto/sha256.test.ts similarity index 55% rename from tasks/jenkins.js rename to src/core/server/utils/crypto/sha256.test.ts index 890fef3442079..ddb8ffee36da6 100644 --- a/tasks/jenkins.js +++ b/src/core/server/utils/crypto/sha256.test.ts @@ -17,25 +17,23 @@ * under the License. */ -module.exports = function (grunt) { - grunt.registerTask('jenkins:docs', ['docker:docs']); +import { createSHA256Hash } from './sha256'; - grunt.registerTask('jenkins:unit', [ - 'run:eslint', - 'run:sasslint', - 'run:checkTsProjects', - 'run:checkDocApiChanges', - 'run:typeCheck', - 'run:i18nCheck', - 'run:telemetryCheck', - 'run:checkFileCasing', - 'run:licenses', - 'run:verifyNotice', - 'run:mocha', - 'run:test_jest', - 'run:test_jest_integration', - 'run:test_projects', - 'run:test_hardening', - 'run:apiIntegrationTests', - ]); -}; +describe('createSHA256Hash', () => { + it('creates a hex-encoded hash by default', () => { + expect(createSHA256Hash('foo')).toMatchInlineSnapshot( + `"2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"` + ); + }); + + it('allows the output encoding to be changed', () => { + expect(createSHA256Hash('foo', 'base64')).toMatchInlineSnapshot( + `"LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564="` + ); + }); + + it('accepts a buffer as input', () => { + const data = Buffer.from('foo', 'utf8'); + expect(createSHA256Hash(data)).toEqual(createSHA256Hash('foo')); + }); +}); diff --git a/src/core/server/utils/crypto/sha256.ts b/src/core/server/utils/crypto/sha256.ts new file mode 100644 index 0000000000000..de9eee2efad5a --- /dev/null +++ b/src/core/server/utils/crypto/sha256.ts @@ -0,0 +1,33 @@ +/* + * 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 crypto, { HexBase64Latin1Encoding } from 'crypto'; + +export const createSHA256Hash = ( + input: string | Buffer, + outputEncoding: HexBase64Latin1Encoding = 'hex' +) => { + let data: Buffer; + if (typeof input === 'string') { + data = Buffer.from(input, 'utf8'); + } else { + data = input; + } + return crypto.createHash('sha256').update(data).digest(outputEncoding); +}; diff --git a/src/dev/build/lib/integration_tests/fs.test.ts b/src/dev/build/lib/integration_tests/fs.test.ts index e9ce09554159b..34d5a15261b6d 100644 --- a/src/dev/build/lib/integration_tests/fs.test.ts +++ b/src/dev/build/lib/integration_tests/fs.test.ts @@ -177,6 +177,16 @@ describe('copyAll()', () => { }); it('copies files and directories from source to dest, creating dest if necessary, respecting mode', async () => { + const path777 = resolve(FIXTURES, 'bin/world_executable'); + const path644 = resolve(FIXTURES, 'foo_dir/bar.txt'); + + // we're seeing flaky failures because the resulting files sometimes have + // 755 permissions. Unless there's a bug in vinyl-fs I can't figure out + // where the issue might be, so trying to validate the mode first to narrow + // down where the issue might be + expect(getCommonMode(path777)).toBe(isWindows ? '666' : '777'); + expect(getCommonMode(path644)).toBe(isWindows ? '666' : '644'); + const destination = resolve(TMP, 'a/b/c'); await copyAll(FIXTURES, destination); @@ -185,10 +195,8 @@ describe('copyAll()', () => { resolve(destination, 'foo_dir/foo'), ]); - expect(getCommonMode(resolve(destination, 'bin/world_executable'))).toBe( - isWindows ? '666' : '777' - ); - expect(getCommonMode(resolve(destination, 'foo_dir/bar.txt'))).toBe(isWindows ? '666' : '644'); + expect(getCommonMode(path777)).toBe(isWindows ? '666' : '777'); + expect(getCommonMode(path644)).toBe(isWindows ? '666' : '644'); }); it('applies select globs if specified, ignores dot files', async () => { diff --git a/src/dev/code_coverage/shell_scripts/extract_archives.sh b/src/dev/code_coverage/shell_scripts/extract_archives.sh index 376467f9f2e55..32b4ccd6abccb 100644 --- a/src/dev/code_coverage/shell_scripts/extract_archives.sh +++ b/src/dev/code_coverage/shell_scripts/extract_archives.sh @@ -6,7 +6,7 @@ EXTRACT_DIR=/tmp/extracted_coverage mkdir -p $EXTRACT_DIR echo "### Extracting downloaded artifacts" -for x in kibana-intake x-pack-intake kibana-oss-tests kibana-xpack-tests; do +for x in kibana-intake kibana-oss-tests kibana-xpack-tests; do #x-pack-intake skipping due to failures tar -xzf $DOWNLOAD_DIR/coverage/${x}/kibana-coverage.tar.gz -C $EXTRACT_DIR || echo "### Error 'tarring': ${x}" done diff --git a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 62b81929ae79b..5d983828394bf 100644 --- a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -32,7 +32,7 @@ TEAM_ASSIGN_PATH=$5 # Build team assignments dat file node scripts/generate_team_assignments.js --verbose --src .github/CODEOWNERS --dest $TEAM_ASSIGN_PATH -for x in jest functional; do +for x in functional; do #jest skip due to failures echo "### Ingesting coverage for ${x}" COVERAGE_SUMMARY_FILE=target/kibana-coverage/${x}-combined/coverage-summary.json diff --git a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh index 707c6de3f88a0..a8952f987b419 100644 --- a/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh +++ b/src/dev/code_coverage/shell_scripts/merge_jest_and_functional.sh @@ -4,6 +4,6 @@ COVERAGE_TEMP_DIR=/tmp/extracted_coverage/target/kibana-coverage/ export COVERAGE_TEMP_DIR echo "### Merge coverage reports" -for x in jest functional; do +for x in functional; do # jest skip due to failures yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.${x}.config.js done diff --git a/src/plugins/advanced_settings/public/management_app/lib/is_default_value.test.ts b/src/plugins/advanced_settings/public/management_app/lib/is_default_value.test.ts index 4cde8bf517499..c8a80bd3d6aab 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/is_default_value.test.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/is_default_value.test.ts @@ -50,9 +50,9 @@ describe('Settings', function () { }); describe('without a value', function () { - it('should return true', function () { + it('should return false for empty string but true for undefined', function () { expect(isDefaultValue({ ...setting, value: undefined })).to.be(true); - expect(isDefaultValue({ ...setting, value: '' })).to.be(true); + expect(isDefaultValue({ ...setting, value: '' })).to.be(false); }); }); diff --git a/src/plugins/advanced_settings/public/management_app/lib/is_default_value.ts b/src/plugins/advanced_settings/public/management_app/lib/is_default_value.ts index 53c2ef3187f09..080aee369dc0f 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/is_default_value.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/is_default_value.ts @@ -22,7 +22,6 @@ export function isDefaultValue(setting: FieldSetting) { return ( setting.isCustom || setting.value === undefined || - setting.value === '' || String(setting.value) === String(setting.defVal) ); } diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index b64b485f65615..68d8a6a42eb5d 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -18,6 +18,9 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], @@ -382,6 +385,9 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], @@ -754,6 +760,9 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap index d68011d2f7fde..e817e898cca67 100644 --- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap @@ -492,6 +492,7 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = ` findItems={[Function]} headingId="dashboardListingHeading" initialFilter="" + initialPageSize={10} listingLimit={1000} noItemsFragment={
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js index 99b1ebf047d74..cc2c0a2e828ca 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js @@ -62,6 +62,7 @@ test('renders empty page in before initial fetch to avoid flickering', () => { getViewUrl={() => {}} listingLimit={1000} hideWriteControls={false} + initialPageSize={10} core={{ notifications: { toasts: {} }, uiSettings: { get: jest.fn(() => 10) } }} /> ); diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx index f9be9d5bfade7..bcd9d31dade26 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { wait } from '@testing-library/dom'; +import { waitFor } from '@testing-library/dom'; import { render } from '@testing-library/react'; import { HelloWorldEmbeddable, @@ -47,7 +47,7 @@ describe('', () => { ); expect(getByTestId('embedSpinner')).toBeInTheDocument(); - await wait(() => !queryByTestId('embedSpinner')); // wait until spinner disappears + await waitFor(() => !queryByTestId('embedSpinner')); // wait until spinner disappears expect(getByTestId('helloWorldEmbeddable')).toBeInTheDocument(); }); }); diff --git a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx index cb14d7ed11dc9..743db62ced989 100644 --- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.test.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { wait, render } from '@testing-library/react'; +import { waitFor, render } from '@testing-library/react'; import { ErrorEmbeddable } from './error_embeddable'; import { EmbeddableRoot } from './embeddable_root'; @@ -26,7 +26,7 @@ test('ErrorEmbeddable renders an embeddable', async () => { const { getByTestId, getByText } = render(); expect(getByTestId('embeddableStackError')).toBeVisible(); - await wait(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component + await waitFor(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component expect(getByText(/some error occurred/i)).toBeVisible(); }); @@ -36,7 +36,7 @@ test('ErrorEmbeddable renders an embeddable with markdown message', async () => const { getByTestId, getByText } = render(); expect(getByTestId('embeddableStackError')).toBeVisible(); - await wait(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component + await waitFor(() => getByTestId('errorMessageMarkdown')); // wait for lazy markdown component expect(getByText(/some link/i)).toMatchInlineSnapshot(` + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ +
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +`; + +exports[`CronEditor is rendered with a HOUR frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`CronEditor is rendered with a MINUTE frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+`; + +exports[`CronEditor is rendered with a MONTH frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + } + labelType="label" + > +
+
+ + + +
+
+ +
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`CronEditor is rendered with a WEEK frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + } + labelType="label" + > +
+
+ + + +
+
+ +
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`CronEditor is rendered with a YEAR frequency 1`] = ` + + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + } + labelType="label" + > +
+
+ + + +
+
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + } + labelType="label" + > +
+
+ + + +
+
+ +
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+ +
+ + +
+ + + +
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/constants.ts b/src/plugins/es_ui_shared/public/components/cron_editor/constants.ts new file mode 100644 index 0000000000000..786e89070d9fb --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/cron_editor/constants.ts @@ -0,0 +1,166 @@ +/* + * 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 { padStart } from 'lodash'; +import { EuiSelectOption } from '@elastic/eui'; + +import { DayOrdinal, MonthOrdinal, getOrdinalValue, getDayName, getMonthName } from './services'; +import { Frequency, Field, FieldToValueMap } from './types'; + +type FieldFlags = { + [key in Field]?: boolean; +}; + +function makeSequence(min: number, max: number): number[] { + const values = []; + for (let i = min; i <= max; i++) { + values.push(i); + } + return values; +} + +export const MINUTE_OPTIONS = makeSequence(0, 59).map((value) => ({ + value: value.toString(), + text: padStart(value.toString(), 2, '0'), +})); + +export const HOUR_OPTIONS = makeSequence(0, 23).map((value) => ({ + value: value.toString(), + text: padStart(value.toString(), 2, '0'), +})); + +export const DAY_OPTIONS = makeSequence(1, 7).map((value) => ({ + value: value.toString(), + text: getDayName((value - 1) as DayOrdinal), +})); + +export const DATE_OPTIONS = makeSequence(1, 31).map((value) => ({ + value: value.toString(), + text: getOrdinalValue(value), +})); + +export const MONTH_OPTIONS = makeSequence(1, 12).map((value) => ({ + value: value.toString(), + text: getMonthName((value - 1) as MonthOrdinal), +})); + +export const UNITS: EuiSelectOption[] = [ + { + value: 'MINUTE', + text: 'minute', + }, + { + value: 'HOUR', + text: 'hour', + }, + { + value: 'DAY', + text: 'day', + }, + { + value: 'WEEK', + text: 'week', + }, + { + value: 'MONTH', + text: 'month', + }, + { + value: 'YEAR', + text: 'year', + }, +]; + +export const frequencyToFieldsMap: Record = { + MINUTE: {}, + HOUR: { + minute: true, + }, + DAY: { + hour: true, + minute: true, + }, + WEEK: { + day: true, + hour: true, + minute: true, + }, + MONTH: { + date: true, + hour: true, + minute: true, + }, + YEAR: { + month: true, + date: true, + hour: true, + minute: true, + }, +}; + +export const frequencyToBaselineFieldsMap: Record = { + MINUTE: { + second: '0', + minute: '*', + hour: '*', + date: '*', + month: '*', + day: '?', + }, + HOUR: { + second: '0', + minute: '0', + hour: '*', + date: '*', + month: '*', + day: '?', + }, + DAY: { + second: '0', + minute: '0', + hour: '0', + date: '*', + month: '*', + day: '?', + }, + WEEK: { + second: '0', + minute: '0', + hour: '0', + date: '?', + month: '*', + day: '7', + }, + MONTH: { + second: '0', + minute: '0', + hour: '0', + date: '1', + month: '*', + day: '?', + }, + YEAR: { + second: '0', + minute: '0', + hour: '0', + date: '1', + month: '1', + day: '?', + }, +}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.tsx similarity index 83% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.tsx index f038766766fe0..42fce194945b9 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_daily.tsx @@ -18,13 +18,25 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + onChange: ({ minute, hour }: { minute?: string; hour?: string }) => void; +} -export const CronDaily = ({ minute, minuteOptions, hour, hourOptions, onChange }) => ( +export const CronDaily: React.FunctionComponent = ({ + minute, + minuteOptions, + hour, + hourOptions, + onChange, +}) => ( ); - -CronDaily.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - hour: PropTypes.string.isRequired, - hourOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx new file mode 100644 index 0000000000000..8d0d497e8b5d4 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx @@ -0,0 +1,137 @@ +/* + * 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 from 'react'; +import sinon from 'sinon'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithI18nProvider } from '@kbn/test/jest'; + +import { Frequency } from './types'; +import { CronEditor } from './cron_editor'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { + return { + htmlIdGenerator: () => () => `generated-id`, + }; +}); + +describe('CronEditor', () => { + ['MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'].forEach((unit) => { + test(`is rendered with a ${unit} frequency`, () => { + const component = mountWithI18nProvider( + {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + }); + + describe('props', () => { + describe('frequencyBlockList', () => { + it('excludes the blocked frequencies from the frequency list', () => { + const component = mountWithI18nProvider( + {}} + /> + ); + + const frequencySelect = findTestSubject(component, 'cronFrequencySelect'); + expect(frequencySelect.text()).toBe('minutedaymonth'); + }); + }); + + describe('cronExpression', () => { + it('sets the values of the fields', () => { + const component = mountWithI18nProvider( + {}} + /> + ); + + const monthSelect = findTestSubject(component, 'cronFrequencyYearlyMonthSelect'); + expect(monthSelect.props().value).toBe('2'); + + const dateSelect = findTestSubject(component, 'cronFrequencyYearlyDateSelect'); + expect(dateSelect.props().value).toBe('5'); + + const hourSelect = findTestSubject(component, 'cronFrequencyYearlyHourSelect'); + expect(hourSelect.props().value).toBe('10'); + + const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect'); + expect(minuteSelect.props().value).toBe('20'); + }); + }); + + describe('onChange', () => { + it('is called when the frequency changes', () => { + const onChangeSpy = sinon.spy(); + const component = mountWithI18nProvider( + + ); + + const frequencySelect = findTestSubject(component, 'cronFrequencySelect'); + frequencySelect.simulate('change', { target: { value: 'MONTH' } }); + + sinon.assert.calledWith(onChangeSpy, { + cronExpression: '0 0 0 1 * ?', + fieldToPreferredValueMap: {}, + frequency: 'MONTH', + }); + }); + + it(`is called when a field's value changes`, () => { + const onChangeSpy = sinon.spy(); + const component = mountWithI18nProvider( + + ); + + const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect'); + minuteSelect.simulate('change', { target: { value: '40' } }); + + sinon.assert.calledWith(onChangeSpy, { + cronExpression: '0 40 * * * ?', + fieldToPreferredValueMap: { minute: '40' }, + frequency: 'YEAR', + }); + }); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.tsx similarity index 58% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.tsx index cde2a253d7630..72e2f51c37e4c 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.tsx @@ -18,207 +18,86 @@ */ import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { padStart } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiSelect, EuiFormRow, EuiSelectOption } from '@elastic/eui'; -import { EuiSelect, EuiFormRow } from '@elastic/eui'; +import { Frequency, Field, FieldToValueMap } from './types'; import { - getOrdinalValue, - getDayName, - getMonthName, - cronExpressionToParts, - cronPartsToExpression, - MINUTE, - HOUR, - DAY, - WEEK, - MONTH, - YEAR, -} from './services'; - + MINUTE_OPTIONS, + HOUR_OPTIONS, + DAY_OPTIONS, + DATE_OPTIONS, + MONTH_OPTIONS, + UNITS, + frequencyToFieldsMap, + frequencyToBaselineFieldsMap, +} from './constants'; + +import { cronExpressionToParts, cronPartsToExpression } from './services'; import { CronHourly } from './cron_hourly'; import { CronDaily } from './cron_daily'; import { CronWeekly } from './cron_weekly'; import { CronMonthly } from './cron_monthly'; import { CronYearly } from './cron_yearly'; -function makeSequence(min, max) { - const values = []; - for (let i = min; i <= max; i++) { - values.push(i); +const excludeBlockListedFrequencies = ( + units: EuiSelectOption[], + blockListedUnits: string[] = [] +): EuiSelectOption[] => { + if (blockListedUnits.length === 0) { + return units; } - return values; -} -const MINUTE_OPTIONS = makeSequence(0, 59).map((value) => ({ - value: value.toString(), - text: padStart(value, 2, '0'), -})); - -const HOUR_OPTIONS = makeSequence(0, 23).map((value) => ({ - value: value.toString(), - text: padStart(value, 2, '0'), -})); - -const DAY_OPTIONS = makeSequence(1, 7).map((value) => ({ - value: value.toString(), - text: getDayName(value - 1), -})); - -const DATE_OPTIONS = makeSequence(1, 31).map((value) => ({ - value: value.toString(), - text: getOrdinalValue(value), -})); - -const MONTH_OPTIONS = makeSequence(1, 12).map((value) => ({ - value: value.toString(), - text: getMonthName(value - 1), -})); - -const UNITS = [ - { - value: MINUTE, - text: 'minute', - }, - { - value: HOUR, - text: 'hour', - }, - { - value: DAY, - text: 'day', - }, - { - value: WEEK, - text: 'week', - }, - { - value: MONTH, - text: 'month', - }, - { - value: YEAR, - text: 'year', - }, -]; - -const frequencyToFieldsMap = { - [MINUTE]: {}, - [HOUR]: { - minute: true, - }, - [DAY]: { - hour: true, - minute: true, - }, - [WEEK]: { - day: true, - hour: true, - minute: true, - }, - [MONTH]: { - date: true, - hour: true, - minute: true, - }, - [YEAR]: { - month: true, - date: true, - hour: true, - minute: true, - }, + return units.filter(({ value }) => !blockListedUnits.includes(value as string)); }; -const frequencyToBaselineFieldsMap = { - [MINUTE]: { - second: '0', - minute: '*', - hour: '*', - date: '*', - month: '*', - day: '?', - }, - [HOUR]: { - second: '0', - minute: '0', - hour: '*', - date: '*', - month: '*', - day: '?', - }, - [DAY]: { - second: '0', - minute: '0', - hour: '0', - date: '*', - month: '*', - day: '?', - }, - [WEEK]: { - second: '0', - minute: '0', - hour: '0', - date: '?', - month: '*', - day: '7', - }, - [MONTH]: { - second: '0', - minute: '0', - hour: '0', - date: '1', - month: '*', - day: '?', - }, - [YEAR]: { - second: '0', - minute: '0', - hour: '0', - date: '1', - month: '1', - day: '?', - }, -}; +interface Props { + frequencyBlockList?: string[]; + fieldToPreferredValueMap: FieldToValueMap; + frequency: Frequency; + cronExpression: string; + onChange: ({ + cronExpression, + fieldToPreferredValueMap, + frequency, + }: { + cronExpression: string; + fieldToPreferredValueMap: FieldToValueMap; + frequency: Frequency; + }) => void; +} -export class CronEditor extends Component { - static propTypes = { - fieldToPreferredValueMap: PropTypes.object.isRequired, - frequency: PropTypes.string.isRequired, - cronExpression: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - }; +type State = FieldToValueMap; - static getDerivedStateFromProps(props) { +export class CronEditor extends Component { + static getDerivedStateFromProps(props: Props) { const { cronExpression } = props; return cronExpressionToParts(cronExpression); } - constructor(props) { + constructor(props: Props) { super(props); const { cronExpression } = props; - const parsedCron = cronExpressionToParts(cronExpression); - this.state = { ...parsedCron, }; } - onChangeFrequency = (frequency) => { + onChangeFrequency = (frequency: Frequency) => { const { onChange, fieldToPreferredValueMap } = this.props; // Update fields which aren't editable with acceptable baseline values. - const editableFields = Object.keys(frequencyToFieldsMap[frequency]); - const inheritedFields = editableFields.reduce( - (baselineFields, field) => { + const editableFields = Object.keys(frequencyToFieldsMap[frequency]) as Field[]; + const inheritedFields = editableFields.reduce( + (fieldBaselines, field) => { if (fieldToPreferredValueMap[field] != null) { - baselineFields[field] = fieldToPreferredValueMap[field]; + fieldBaselines[field] = fieldToPreferredValueMap[field]; } - return baselineFields; + return fieldBaselines; }, { ...frequencyToBaselineFieldsMap[frequency] } ); @@ -232,18 +111,21 @@ export class CronEditor extends Component { }); }; - onChangeFields = (fields) => { + onChangeFields = (fields: FieldToValueMap) => { const { onChange, frequency, fieldToPreferredValueMap } = this.props; - const editableFields = Object.keys(frequencyToFieldsMap[frequency]); - const newFieldToPreferredValueMap = {}; + const editableFields = Object.keys(frequencyToFieldsMap[frequency]) as Field[]; + const newFieldToPreferredValueMap: FieldToValueMap = {}; - const editedFields = editableFields.reduce( + const editedFields = editableFields.reduce( (accumFields, field) => { if (fields[field] !== undefined) { accumFields[field] = fields[field]; - // Once the user touches a field, we want to persist its value as the user changes - // the cron frequency. + // If the user changes a field's value, we want to maintain that value in the relevant + // field, even as the frequency field changes. For example, if the user selects "Monthly" + // frequency and changes the "Hour" field to "10", that field should still say "10" if the + // user changes the frequency to "Weekly". We'll support this UX by storing these values + // in the fieldToPreferredValueMap. newFieldToPreferredValueMap[field] = fields[field]; } else { accumFields[field] = this.state[field]; @@ -271,10 +153,10 @@ export class CronEditor extends Component { const { minute, hour, day, date, month } = this.state; switch (frequency) { - case MINUTE: + case 'MINUTE': return; - case HOUR: + case 'HOUR': return ( ); - case DAY: + case 'DAY': return ( ); - case WEEK: + case 'WEEK': return ( ); - case MONTH: + case 'MONTH': return ( ); - case YEAR: + case 'YEAR': return ( @@ -352,9 +234,11 @@ export class CronEditor extends Component { fullWidth > this.onChangeFrequency(e.target.value)} + onChange={(e: React.ChangeEvent) => + this.onChangeFrequency(e.target.value as Frequency) + } fullWidth prepend={i18n.translate('esUi.cronEditor.textEveryLabel', { defaultMessage: 'Every', diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.tsx similarity index 83% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.tsx index a04e83195b97f..fb793fd4ff605 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_hourly.tsx @@ -18,13 +18,17 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -import { EuiFormRow, EuiSelect } from '@elastic/eui'; +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + onChange: ({ minute }: { minute?: string }) => void; +} -export const CronHourly = ({ minute, minuteOptions, onChange }) => ( +export const CronHourly: React.FunctionComponent = ({ minute, minuteOptions, onChange }) => ( ( ); - -CronHourly.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.tsx similarity index 86% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.tsx index 28057bd7d9293..729ef1f5f0c15 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_monthly.tsx @@ -18,13 +18,21 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + date?: string; + dateOptions: EuiSelectOption[]; + onChange: ({ minute, hour, date }: { minute?: string; hour?: string; date?: string }) => void; +} -export const CronMonthly = ({ +export const CronMonthly: React.FunctionComponent = ({ minute, minuteOptions, hour, @@ -94,13 +102,3 @@ export const CronMonthly = ({ ); - -CronMonthly.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - hour: PropTypes.string.isRequired, - hourOptions: PropTypes.array.isRequired, - date: PropTypes.string.isRequired, - dateOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.tsx similarity index 86% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.tsx index c06eecbb381b3..1f10ba5a4ab84 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_weekly.tsx @@ -18,13 +18,21 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + day?: string; + dayOptions: EuiSelectOption[]; + onChange: ({ minute, hour, day }: { minute?: string; hour?: string; day?: string }) => void; +} -export const CronWeekly = ({ +export const CronWeekly: React.FunctionComponent = ({ minute, minuteOptions, hour, @@ -94,13 +102,3 @@ export const CronWeekly = ({ ); - -CronWeekly.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - hour: PropTypes.string.isRequired, - hourOptions: PropTypes.array.isRequired, - day: PropTypes.string.isRequired, - dayOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.js b/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.tsx similarity index 86% rename from src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.js rename to src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.tsx index c3b9691750937..8b65a6f77cfc0 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_yearly.tsx @@ -18,13 +18,34 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui'; -export const CronYearly = ({ +interface Props { + minute?: string; + minuteOptions: EuiSelectOption[]; + hour?: string; + hourOptions: EuiSelectOption[]; + date?: string; + dateOptions: EuiSelectOption[]; + month?: string; + monthOptions: EuiSelectOption[]; + onChange: ({ + minute, + hour, + date, + month, + }: { + minute?: string; + hour?: string; + date?: string; + month?: string; + }) => void; +} + +export const CronYearly: React.FunctionComponent = ({ minute, minuteOptions, hour, @@ -115,15 +136,3 @@ export const CronYearly = ({ ); - -CronYearly.propTypes = { - minute: PropTypes.string.isRequired, - minuteOptions: PropTypes.array.isRequired, - hour: PropTypes.string.isRequired, - hourOptions: PropTypes.array.isRequired, - date: PropTypes.string.isRequired, - dateOptions: PropTypes.array.isRequired, - month: PropTypes.string.isRequired, - monthOptions: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, -}; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/index.js b/src/plugins/es_ui_shared/public/components/cron_editor/index.ts similarity index 92% rename from src/plugins/es_ui_shared/public/components/cron_editor/index.js rename to src/plugins/es_ui_shared/public/components/cron_editor/index.ts index 6c4539a6c3f75..b1e27feb6f835 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/index.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/index.ts @@ -17,5 +17,5 @@ * under the License. */ +export { Frequency } from './types'; export { CronEditor } from './cron_editor'; -export { MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from './services'; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.js b/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.ts similarity index 81% rename from src/plugins/es_ui_shared/public/components/cron_editor/services/cron.js rename to src/plugins/es_ui_shared/public/components/cron_editor/services/cron.ts index 995169739f7dc..be78552584148 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/services/cron.ts @@ -17,15 +17,10 @@ * under the License. */ -export const MINUTE = 'MINUTE'; -export const HOUR = 'HOUR'; -export const DAY = 'DAY'; -export const WEEK = 'WEEK'; -export const MONTH = 'MONTH'; -export const YEAR = 'YEAR'; +import { FieldToValueMap } from '../types'; -export function cronExpressionToParts(expression) { - const parsedCron = { +export function cronExpressionToParts(expression: string): FieldToValueMap { + const parsedCron: FieldToValueMap = { second: undefined, minute: undefined, hour: undefined, @@ -63,6 +58,13 @@ export function cronExpressionToParts(expression) { return parsedCron; } -export function cronPartsToExpression({ second, minute, hour, day, date, month }) { +export function cronPartsToExpression({ + second, + minute, + hour, + day, + date, + month, +}: FieldToValueMap): string { return `${second} ${minute} ${hour} ${date} ${month} ${day}`; } diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js b/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.ts similarity index 87% rename from src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js rename to src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.ts index 69fa085cc3f3e..25ac0db3d35d8 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.js +++ b/src/plugins/es_ui_shared/public/components/cron_editor/services/humanized_numbers.ts @@ -19,6 +19,9 @@ import { i18n } from '@kbn/i18n'; +export type DayOrdinal = 0 | 1 | 2 | 3 | 4 | 5 | 6; +export type MonthOrdinal = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; + // The international ISO standard dictates Monday as the first day of the week, but cron patterns // use Sunday as the first day, so we're going with the cron way. const dayOrdinalToDayNameMap = { @@ -46,7 +49,7 @@ const monthOrdinalToMonthNameMap = { 11: i18n.translate('esUi.cronEditor.month.december', { defaultMessage: 'December' }), }; -export function getOrdinalValue(number) { +export function getOrdinalValue(number: number): string { // TODO: This is breaking reporting pdf generation. Possibly due to phantom not setting locale, // which is needed by i18n (formatjs). Need to verify, fix, and restore i18n in place of static stings. // return i18n.translate('esUi.cronEditor.number.ordinal', { @@ -57,15 +60,16 @@ export function getOrdinalValue(number) { // Protects against falsey (including 0) values const num = number && number.toString(); - let lastDigit = num && num.substr(-1); + const lastDigitString = num && num.substr(-1); let ordinal; - if (!lastDigit) { - return number; + if (!lastDigitString) { + return number.toString(); } - lastDigit = parseFloat(lastDigit); - switch (lastDigit) { + const lastDigitNumeric = parseFloat(lastDigitString); + + switch (lastDigitNumeric) { case 1: ordinal = 'st'; break; @@ -82,10 +86,10 @@ export function getOrdinalValue(number) { return `${num}${ordinal}`; } -export function getDayName(dayOrdinal) { +export function getDayName(dayOrdinal: DayOrdinal): string { return dayOrdinalToDayNameMap[dayOrdinal]; } -export function getMonthName(monthOrdinal) { +export function getMonthName(monthOrdinal: MonthOrdinal): string { return monthOrdinalToMonthNameMap[monthOrdinal]; } diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/services/index.ts b/src/plugins/es_ui_shared/public/components/cron_editor/services/index.ts new file mode 100644 index 0000000000000..ff10a283c2fa1 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/cron_editor/services/index.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. + */ + +export { cronExpressionToParts, cronPartsToExpression } from './cron'; +export { + getOrdinalValue, + getDayName, + getMonthName, + DayOrdinal, + MonthOrdinal, +} from './humanized_numbers'; diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/types.ts b/src/plugins/es_ui_shared/public/components/cron_editor/types.ts new file mode 100644 index 0000000000000..3e5b7c916632a --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/cron_editor/types.ts @@ -0,0 +1,24 @@ +/* + * 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 type Frequency = 'MINUTE' | 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'YEAR'; +export type Field = 'second' | 'minute' | 'hour' | 'day' | 'date' | 'month'; +export type FieldToValueMap = { + [key in Field]?: string; +}; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index f48198459d48d..304916b1d379d 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -30,7 +30,7 @@ export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './compon export { SectionLoading } from './components/section_loading'; -export { CronEditor, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from './components/cron_editor'; +export { Frequency, CronEditor } from './components/cron_editor'; export { SendRequestConfig, diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.ts b/src/plugins/es_ui_shared/public/request/use_request.test.ts index 2a639f93b47b4..822bf56e5e3cc 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.test.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.test.ts @@ -101,8 +101,9 @@ describe('useRequest hook', () => { const { setupSuccessRequest, completeRequest, hookResult } = helpers; setupSuccessRequest(); expect(hookResult.isInitialRequest).toBe(true); - - hookResult.resendRequest(); + act(() => { + hookResult.resendRequest(); + }); await completeRequest(); expect(hookResult.isInitialRequest).toBe(false); }); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap index 29cbec38a5982..d24b31599f903 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -54,7 +54,7 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` @@ -294,7 +294,7 @@ exports[`FieldEditor should render edit scripted field correctly 1`] = ` @@ -586,7 +586,7 @@ exports[`FieldEditor should show conflict field warning 1`] = ` @@ -827,7 +827,7 @@ exports[`FieldEditor should show deprecated lang warning 1`] = ` @@ -1200,7 +1200,7 @@ exports[`FieldEditor should show multiple type field warning with a table contai diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 29a87a65fdff7..a402dc59185e8 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -425,7 +425,7 @@ export class FieldEditor extends PureComponent } > 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 d06fd0df98a8c..a48965cf7f41c 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 @@ -180,6 +180,9 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index cad06255ffe98..896b1671328a9 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -321,6 +321,9 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO "serverBasePath": "", }, "delete": [MockFunction], + "externalUrl": Object { + "validateUrl": [MockFunction], + }, "fetch": [MockFunction], "get": [MockFunction], "getLoadingCount$": [MockFunction], diff --git a/src/plugins/telemetry_management_section/public/components/opt_in_example_flyout.tsx b/src/plugins/telemetry_management_section/public/components/opt_in_example_flyout.tsx index 402fc55eaf7c3..bd78cdc931d0a 100644 --- a/src/plugins/telemetry_management_section/public/components/opt_in_example_flyout.tsx +++ b/src/plugins/telemetry_management_section/public/components/opt_in_example_flyout.tsx @@ -49,6 +49,8 @@ interface State { * React component for displaying the example data associated with the Telemetry opt-in banner. */ export class OptInExampleFlyout extends React.PureComponent { + _isMounted = false; + public readonly state: State = { data: null, isLoading: true, @@ -56,14 +58,18 @@ export class OptInExampleFlyout extends React.PureComponent { }; async componentDidMount() { + this._isMounted = true; + try { const { fetchExample } = this.props; const clusters = await fetchExample(); - this.setState({ - data: Array.isArray(clusters) ? clusters : null, - isLoading: false, - hasPrivilegeToRead: true, - }); + if (this._isMounted) { + this.setState({ + data: Array.isArray(clusters) ? clusters : null, + isLoading: false, + hasPrivilegeToRead: true, + }); + } } catch (err) { this.setState({ isLoading: false, @@ -72,6 +78,10 @@ export class OptInExampleFlyout extends React.PureComponent { } } + componentWillUnmount() { + this._isMounted = false; + } + renderBody({ data, isLoading, hasPrivilegeToRead }: State) { if (isLoading) { return loadingSpinner; diff --git a/src/plugins/ui_actions/kibana.json b/src/plugins/ui_actions/kibana.json index 337c5ddf0fd5c..ca979aa021026 100644 --- a/src/plugins/ui_actions/kibana.json +++ b/src/plugins/ui_actions/kibana.json @@ -3,9 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "extraPublicDirs": [ - "public/tests/test_samples" - ], "requiredBundles": [ "kibanaUtils", "kibanaReact" diff --git a/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx b/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx index 7bc8cdbd14170..e9494e086a734 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx +++ b/src/plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { wait, render } from '@testing-library/react'; +import { waitFor, render } from '@testing-library/react'; import MarkdownVisComponent from './markdown_vis_controller'; describe('markdown vis controller', () => { @@ -36,7 +36,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(getByText('markdown')).toMatchInlineSnapshot(`
{ ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(getByText(/testing/i)).toMatchInlineSnapshot(`

@@ -82,7 +82,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(getByText(/initial/i)).toBeInTheDocument(); @@ -112,7 +112,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(renderComplete).toHaveBeenCalledTimes(1); }); @@ -122,7 +122,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(renderComplete).toHaveBeenCalledTimes(1); @@ -139,7 +139,7 @@ describe('markdown vis controller', () => { ); - await wait(() => getByTestId('markdownBody')); + await waitFor(() => getByTestId('markdownBody')); expect(renderComplete).toHaveBeenCalledTimes(1); diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index 46f46eaa3026f..0e9196eb165e9 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -49,13 +49,15 @@ export class ValidationTelemetryService implements Plugin({ type: 'tsvb-validation', isReady: () => this.kibanaIndex !== '', - fetch: async ({ callCluster }) => { + fetch: async ({ esClient }) => { try { - const response = await callCluster('get', { - index: this.kibanaIndex, - id: 'tsvb-validation-telemetry:tsvb-validation-telemetry', - ignore: [404], - }); + const { body: response } = await esClient.get( + { + index: this.kibanaIndex, + id: 'tsvb-validation-telemetry:tsvb-validation-telemetry', + }, + { ignore: [404] } + ); return { failed_validations: response?._source?.['tsvb-validation-telemetry']?.failedRequests || 0, diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index 8a073ca32b94a..e4c4c1df202ef 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -17,6 +17,8 @@ * under the License. */ +import 'jest-canvas-mock'; + import $ from 'jquery'; import 'leaflet/dist/leaflet.js'; diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.mock.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.mock.ts index 70be4c273b77f..95ba522c7f092 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.mock.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.mock.ts @@ -19,6 +19,7 @@ export const mockStats = { somestat: 1 }; export const mockGetStats = jest.fn().mockResolvedValue(mockStats); + jest.doMock('./get_usage_collector', () => ({ getStats: mockGetStats, })); diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts index 6f17703bc9dee..fcf5deab26c8c 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts @@ -72,7 +72,8 @@ const mockedSavedObjects = [ const getMockCollectorFetchContext = (hits?: unknown[]) => { const fetchParamsMock = createCollectorFetchContextMock(); - fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + + fetchParamsMock.esClient.search = jest.fn().mockResolvedValue({ body: { hits: { hits } } }); return fetchParamsMock; }; @@ -104,17 +105,13 @@ describe('Vega visualization usage collector', () => { }; test('Returns undefined when no results found (undefined)', async () => { - const result = await getStats(getMockCollectorFetchContext().callCluster, mockIndex, mockDeps); + const result = await getStats(getMockCollectorFetchContext().esClient, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Returns undefined when no results found (0 results)', async () => { - const result = await getStats( - getMockCollectorFetchContext([]).callCluster, - mockIndex, - mockDeps - ); + const result = await getStats(getMockCollectorFetchContext([]).esClient, mockIndex, mockDeps); expect(result).toBeUndefined(); }); @@ -129,7 +126,7 @@ describe('Vega visualization usage collector', () => { }, }, ]); - const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.esClient, mockIndex, mockDeps); expect(result).toBeUndefined(); }); @@ -153,14 +150,14 @@ describe('Vega visualization usage collector', () => { }, ]); - const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.esClient, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Summarizes visualizations response data', async () => { const mockCollectorFetchContext = getMockCollectorFetchContext(mockedSavedObjects); - const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.esClient, mockIndex, mockDeps); expect(result).toMatchObject({ vega_lib_specs_total: 2, diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts index b0de8eb2f5140..93634ea0c0886 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts @@ -18,7 +18,7 @@ */ import { parse } from 'hjson'; import { SearchResponse } from 'elasticsearch'; -import { LegacyAPICaller, SavedObject } from 'src/core/server'; +import { ElasticsearchClient, SavedObject } from 'src/core/server'; import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types'; @@ -64,7 +64,7 @@ export interface VegaUsage { } export const getStats = async ( - callCluster: LegacyAPICaller, + esClient: ElasticsearchClient, index: string, { home }: UsageCollectorDependencies ): Promise => { @@ -90,7 +90,7 @@ export const getStats = async ( }, }; - const esResponse: ESResponse = await callCluster('search', searchParams); + const { body: esResponse } = await esClient.search(searchParams); const size = esResponse?.hits?.hits?.length ?? 0; if (!size) { diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts index fbef55df39719..a35d822b35640 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -63,11 +63,7 @@ describe('registerVegaUsageCollector', () => { const mockedCollectorFetchContext = createCollectorFetchContextMock(); const fetchResult = await usageCollector.fetch(mockedCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); - expect(mockGetStats).toBeCalledWith( - mockedCollectorFetchContext.callCluster, - mockIndex, - mockDeps - ); + expect(mockGetStats).toBeCalledWith(mockedCollectorFetchContext.esClient, mockIndex, mockDeps); expect(fetchResult).toBe(mockStats); }); }); diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts index e4772dad99d40..b22bb156510f1 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts @@ -35,10 +35,10 @@ export function registerVegaUsageCollector( vega_lite_lib_specs_total: { type: 'long' }, vega_use_map_total: { type: 'long' }, }, - fetch: async ({ callCluster }) => { + fetch: async ({ esClient }) => { const { index } = (await config.pipe(first()).toPromise()).kibana; - return await getStats(callCluster, index, dependencies); + return await getStats(esClient, index, dependencies); }, }); 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 a3fb536d0aec5..7acc97404c11c 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 @@ -25,6 +25,7 @@ import { EuiButtonGroup } from '@elastic/eui'; import { VisLegend, VisLegendProps } from './legend'; import { legendColors } from './models'; +import { act } from '@testing-library/react'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -206,7 +207,9 @@ describe('VisLegend Component', () => { const first = getLegendItems(wrapper).first(); first.simulate('click'); const filterGroup = wrapper.find(EuiButtonGroup).first(); - filterGroup.getElement().props.onChange('filterIn'); + act(() => { + filterGroup.getElement().props.onChange('filterIn'); + }); expect(fireEvent).toHaveBeenCalledWith({ name: 'filterBucket', @@ -219,7 +222,9 @@ describe('VisLegend Component', () => { const first = getLegendItems(wrapper).first(); first.simulate('click'); const filterGroup = wrapper.find(EuiButtonGroup).first(); - filterGroup.getElement().props.onChange('filterOut'); + act(() => { + filterGroup.getElement().props.onChange('filterOut'); + }); expect(fireEvent).toHaveBeenCalledWith({ name: 'filterBucket', diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts index fd363b3505186..970948fad52cb 100644 --- a/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts @@ -18,7 +18,7 @@ */ import moment from 'moment'; -import { LegacyAPICaller } from 'src/core/server'; +import { ElasticsearchClient } from 'src/core/server'; import { getStats } from './get_usage_collector'; const defaultMockSavedObjects = [ @@ -120,8 +120,11 @@ const enlargedMockSavedObjects = [ describe('Visualizations usage collector', () => { const mockIndex = ''; + const getMockCallCluster = (hits: unknown[]) => - (() => Promise.resolve({ hits: { hits } }) as unknown) as LegacyAPICaller; + ({ + search: () => Promise.resolve({ body: { hits: { hits } } }) as unknown, + } as ElasticsearchClient); test('Returns undefined when no results found (undefined)', async () => { const result = await getStats(getMockCallCluster(undefined as any), mockIndex); diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts index aed9a54dcf01a..83ae67347e915 100644 --- a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts @@ -18,11 +18,10 @@ */ import { countBy, get, groupBy, mapValues, max, min, values } from 'lodash'; +import { ElasticsearchClient } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'src/core/server'; import { getPastDays } from './get_past_days'; - type ESResponse = SearchResponse<{ visualization: { visState: string } }>; interface VisSummary { @@ -47,7 +46,7 @@ export interface VisualizationUsage { * Parse the response data into telemetry payload */ export async function getStats( - callCluster: LegacyAPICaller, + esClient: ElasticsearchClient, index: string ): Promise { const searchParams = { @@ -65,7 +64,7 @@ export async function getStats( }, }, }; - const esResponse: ESResponse = await callCluster('search', searchParams); + const { body: esResponse } = await esClient.search(searchParams); const size = get(esResponse, 'hits.hits.length', 0); if (size < 1) { return; diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts index 380a86e15aa51..5d65666da544d 100644 --- a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts @@ -62,7 +62,7 @@ describe('registerVisualizationsCollector', () => { const mockCollectorFetchContext = createCollectorFetchContextMock(); const fetchResult = await usageCollector.fetch(mockCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); - expect(mockGetStats).toBeCalledWith(mockCollectorFetchContext.callCluster, mockIndex); + expect(mockGetStats).toBeCalledWith(mockCollectorFetchContext.esClient, mockIndex); expect(fetchResult).toBe(mockStats); }); }); diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts index 4188f564ed5fd..11c562f764ce8 100644 --- a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts @@ -41,9 +41,9 @@ export function registerVisualizationsCollector( saved_90_days_total: { type: 'long' }, }, }, - fetch: async ({ callCluster }) => { + fetch: async ({ esClient }) => { const index = (await config.pipe(first()).toPromise()).kibana.index; - return await getStats(callCluster, index); + return await getStats(esClient, index); }, }); collectorSet.registerCollector(collector); diff --git a/tasks/config/run.js b/tasks/config/run.js deleted file mode 100644 index 0a1bb9617e1f9..0000000000000 --- a/tasks/config/run.js +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { version } = require('../../package.json'); -const KIBANA_INSTALL_DIR = - process.env.KIBANA_INSTALL_DIR || - `./build/oss/kibana-${version}-SNAPSHOT-${process.platform}-x86_64`; - -module.exports = function () { - const NODE = 'node'; - const YARN = 'yarn'; - const scriptWithGithubChecks = ({ title, options, cmd, args }) => - process.env.CHECKS_REPORTER_ACTIVE === 'true' - ? { - options, - cmd: YARN, - args: ['run', 'github-checks-reporter', title, cmd, ...args], - } - : { options, cmd, args }; - const gruntTaskWithGithubChecks = (title, task) => - scriptWithGithubChecks({ - title, - cmd: YARN, - args: ['run', 'grunt', task], - }); - - return { - // used by the test and jenkins:unit tasks - // runs the eslint script to check for linting errors - eslint: scriptWithGithubChecks({ - title: 'eslint', - cmd: NODE, - args: ['scripts/eslint', '--no-cache'], - }), - - sasslint: scriptWithGithubChecks({ - title: 'sasslint', - cmd: NODE, - args: ['scripts/sasslint'], - }), - - // used by the test tasks - // runs the check_file_casing script to ensure filenames use correct casing - checkFileCasing: scriptWithGithubChecks({ - title: 'Check file casing', - cmd: NODE, - args: [ - 'scripts/check_file_casing', - '--quiet', // only log errors, not warnings - ], - }), - - // used by the test tasks - // runs the check_published_api_changes script to ensure API changes are explictily accepted - checkDocApiChanges: scriptWithGithubChecks({ - title: 'Check core API changes', - cmd: NODE, - args: ['scripts/check_published_api_changes'], - }), - - // used by the test and jenkins:unit tasks - // runs the typecheck script to check for Typescript type errors - typeCheck: scriptWithGithubChecks({ - title: 'Type check', - cmd: NODE, - args: ['scripts/type_check'], - }), - - // used by the test and jenkins:unit tasks - // ensures that all typescript files belong to a typescript project - checkTsProjects: scriptWithGithubChecks({ - title: 'TypeScript - all files belong to a TypeScript project', - cmd: NODE, - args: ['scripts/check_ts_projects'], - }), - - // used by the test and jenkins:unit tasks - // runs the i18n_check script to check i18n engine usage - i18nCheck: scriptWithGithubChecks({ - title: 'Internationalization check', - cmd: NODE, - args: ['scripts/i18n_check', '--ignore-missing'], - }), - - telemetryCheck: scriptWithGithubChecks({ - title: 'Telemetry Schema check', - cmd: NODE, - args: ['scripts/telemetry_check'], - }), - - // used by the test:quick task - // runs all node.js/server mocha tests - mocha: scriptWithGithubChecks({ - title: 'Mocha tests', - cmd: NODE, - args: ['scripts/mocha'], - }), - - // used by the test:mochaCoverage task - mochaCoverage: scriptWithGithubChecks({ - title: 'Mocha tests coverage', - cmd: YARN, - args: [ - 'nyc', - '--reporter=html', - '--reporter=json-summary', - '--report-dir=./target/kibana-coverage/mocha', - NODE, - 'scripts/mocha', - ], - }), - - verifyNotice: scriptWithGithubChecks({ - title: 'Verify NOTICE.txt', - options: { - wait: true, - }, - cmd: NODE, - args: ['scripts/notice', '--validate'], - }), - - test_hardening: scriptWithGithubChecks({ - title: 'Node.js hardening tests', - cmd: NODE, - args: ['scripts/test_hardening.js'], - }), - - apiIntegrationTests: scriptWithGithubChecks({ - title: 'API integration tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/api_integration/config.js', - '--bail', - '--debug', - ], - }), - - serverIntegrationTests: scriptWithGithubChecks({ - title: 'Server integration tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/server_integration/http/ssl/config.js', - '--config', - 'test/server_integration/http/ssl_redirect/config.js', - '--config', - 'test/server_integration/http/platform/config.ts', - '--config', - 'test/server_integration/http/ssl_with_p12/config.js', - '--config', - 'test/server_integration/http/ssl_with_p12_intermediate/config.js', - '--bail', - '--debug', - '--kibana-install-dir', - KIBANA_INSTALL_DIR, - ], - }), - - interpreterFunctionalTestsRelease: scriptWithGithubChecks({ - title: 'Interpreter functional tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/interpreter_functional/config.ts', - '--bail', - '--debug', - '--kibana-install-dir', - KIBANA_INSTALL_DIR, - ], - }), - - pluginFunctionalTestsRelease: scriptWithGithubChecks({ - title: 'Plugin functional tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/plugin_functional/config.ts', - '--bail', - '--debug', - ], - }), - - exampleFunctionalTestsRelease: scriptWithGithubChecks({ - title: 'Example functional tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/examples/config.js', - '--bail', - '--debug', - ], - }), - - functionalTests: scriptWithGithubChecks({ - title: 'Functional tests', - cmd: NODE, - args: [ - 'scripts/functional_tests', - '--config', - 'test/functional/config.js', - '--bail', - '--debug', - ], - }), - - licenses: scriptWithGithubChecks({ - title: 'Check licenses', - cmd: NODE, - args: ['scripts/check_licenses', '--dev'], - }), - - test_jest: gruntTaskWithGithubChecks('Jest tests', 'test:jest'), - test_jest_integration: gruntTaskWithGithubChecks( - 'Jest integration tests', - 'test:jest_integration' - ), - test_projects: gruntTaskWithGithubChecks('Project tests', 'test:projects'), - }; -}; diff --git a/tasks/test.js b/tasks/test.js deleted file mode 100644 index f370ea0b948c6..0000000000000 --- a/tasks/test.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { run } from '../utilities/visual_regression'; - -module.exports = function (grunt) { - grunt.registerTask( - 'test:visualRegression:buildGallery', - 'Compare screenshots and generate diff images.', - function () { - const done = this.async(); - run(done); - } - ); - - grunt.registerTask('test:quick', [ - 'checkPlugins', - 'run:mocha', - 'run:functionalTests', - 'test:jest', - 'test:jest_integration', - 'test:projects', - 'run:apiIntegrationTests', - ]); - - grunt.registerTask('test:mochaCoverage', ['run:mochaCoverage']); - - grunt.registerTask('test', (subTask) => { - if (subTask) grunt.fail.fatal(`invalid task "test:${subTask}"`); - - grunt.task.run( - [ - !grunt.option('quick') && 'run:eslint', - !grunt.option('quick') && 'run:sasslint', - !grunt.option('quick') && 'run:checkTsProjects', - !grunt.option('quick') && 'run:checkDocApiChanges', - !grunt.option('quick') && 'run:typeCheck', - !grunt.option('quick') && 'run:i18nCheck', - 'run:checkFileCasing', - 'run:licenses', - 'test:quick', - ].filter(Boolean) - ); - }); - - grunt.registerTask('quick-test', ['test:quick']); // historical alias - - grunt.registerTask('test:projects', function () { - const done = this.async(); - runProjectsTests().then(done, done); - }); - - function runProjectsTests() { - const serverCmd = { - cmd: 'yarn', - args: ['kbn', 'run', 'test', '--exclude', 'kibana', '--oss', '--skip-kibana-plugins'], - opts: { stdio: 'inherit' }, - }; - - return new Promise((resolve, reject) => { - grunt.util.spawn(serverCmd, (error, result, code) => { - if (error || code !== 0) { - const error = new Error(`projects tests exited with code ${code}`); - grunt.fail.fatal(error); - reject(error); - return; - } - - grunt.log.writeln(result); - resolve(); - }); - }); - } -}; diff --git a/tasks/test_jest.js b/tasks/test_jest.js deleted file mode 100644 index 810ed42324840..0000000000000 --- a/tasks/test_jest.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { resolve } = require('path'); - -module.exports = function (grunt) { - grunt.registerTask('test:jest', function () { - const done = this.async(); - runJest(resolve(__dirname, '../scripts/jest.js'), ['--maxWorkers=10']).then(done, done); - }); - - grunt.registerTask('test:jest_integration', function () { - const done = this.async(); - runJest(resolve(__dirname, '../scripts/jest_integration.js')).then(done, done); - }); - - function runJest(jestScript, args = []) { - const serverCmd = { - cmd: 'node', - args: [jestScript, '--ci', ...args], - opts: { stdio: 'inherit' }, - }; - - return new Promise((resolve, reject) => { - grunt.util.spawn(serverCmd, (error, result, code) => { - if (error || code !== 0) { - const error = new Error(`jest exited with code ${code}`); - grunt.fail.fatal(error); - reject(error); - return; - } - - grunt.log.writeln(result); - resolve(); - }); - }); - } -}; diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index f52343a9d913b..bd084fe1fb081 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -467,6 +467,13 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo await button.click(); } } + + /** + * Get visible text of the Welcome Banner + */ + async getWelcomeText() { + return await testSubjects.getVisibleText('global-banner-item'); + } } return new CommonPage(); diff --git a/test/scripts/checks/bundle_limits.sh b/test/scripts/checks/bundle_limits.sh index 10d9d9343fda4..cfe08d73bb558 100755 --- a/test/scripts/checks/bundle_limits.sh +++ b/test/scripts/checks/bundle_limits.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -node scripts/build_kibana_platform_plugins --validate-limits +checks-reporter-with-killswitch "Check Bundle Limits" \ + node scripts/build_kibana_platform_plugins --validate-limits diff --git a/test/scripts/checks/doc_api_changes.sh b/test/scripts/checks/doc_api_changes.sh index 503d12b2f6d73..f2f508fd8f7d4 100755 --- a/test/scripts/checks/doc_api_changes.sh +++ b/test/scripts/checks/doc_api_changes.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:checkDocApiChanges +checks-reporter-with-killswitch "Check Doc API Changes" \ + node scripts/check_published_api_changes diff --git a/test/scripts/checks/file_casing.sh b/test/scripts/checks/file_casing.sh index 513664263791b..b30dfaab62a98 100755 --- a/test/scripts/checks/file_casing.sh +++ b/test/scripts/checks/file_casing.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:checkFileCasing +checks-reporter-with-killswitch "Check File Casing" \ + node scripts/check_file_casing --quiet diff --git a/test/scripts/checks/i18n.sh b/test/scripts/checks/i18n.sh index 7a6fd46c46c76..e7a2060aaa73a 100755 --- a/test/scripts/checks/i18n.sh +++ b/test/scripts/checks/i18n.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:i18nCheck +checks-reporter-with-killswitch "Check i18n" \ + node scripts/i18n_check --ignore-missing diff --git a/test/scripts/checks/jest_configs.sh b/test/scripts/checks/jest_configs.sh old mode 100644 new mode 100755 index 28cb1386c748f..67fbee0b9fdf0 --- a/test/scripts/checks/jest_configs.sh +++ b/test/scripts/checks/jest_configs.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -checks-reporter-with-killswitch "Check Jest Configs" node scripts/check_jest_configs +checks-reporter-with-killswitch "Check Jest Configs" \ + node scripts/check_jest_configs diff --git a/test/scripts/checks/licenses.sh b/test/scripts/checks/licenses.sh index a08d7d07a24a1..22494f11ce77c 100755 --- a/test/scripts/checks/licenses.sh +++ b/test/scripts/checks/licenses.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:licenses +checks-reporter-with-killswitch "Check Licenses" \ + node scripts/check_licenses --dev diff --git a/test/scripts/checks/mocha_coverage.sh b/test/scripts/checks/mocha_coverage.sh new file mode 100755 index 0000000000000..e1afad0ab775f --- /dev/null +++ b/test/scripts/checks/mocha_coverage.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +yarn nyc --reporter=html --reporter=json-summary --report-dir=./target/kibana-coverage/mocha node scripts/mocha diff --git a/test/scripts/checks/plugins_with_circular_deps.sh b/test/scripts/checks/plugins_with_circular_deps.sh old mode 100644 new mode 100755 index 77880243538d2..a608d7e7b2edf --- a/test/scripts/checks/plugins_with_circular_deps.sh +++ b/test/scripts/checks/plugins_with_circular_deps.sh @@ -2,5 +2,5 @@ source src/dev/ci_setup/setup_env.sh -checks-reporter-with-killswitch "Check plugins with circular dependencies" \ +checks-reporter-with-killswitch "Check Plugins With Circular Dependencies" \ node scripts/find_plugins_with_circular_deps diff --git a/test/scripts/checks/telemetry.sh b/test/scripts/checks/telemetry.sh index c74ec295b385c..1622704b1fa92 100755 --- a/test/scripts/checks/telemetry.sh +++ b/test/scripts/checks/telemetry.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:telemetryCheck +checks-reporter-with-killswitch "Check Telemetry Schema" \ + node scripts/telemetry_check diff --git a/test/scripts/checks/test_hardening.sh b/test/scripts/checks/test_hardening.sh index 9184758577654..cd0c5a7d3c3aa 100755 --- a/test/scripts/checks/test_hardening.sh +++ b/test/scripts/checks/test_hardening.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:test_hardening +checks-reporter-with-killswitch "Test Hardening" \ + node scripts/test_hardening diff --git a/test/scripts/checks/test_projects.sh b/test/scripts/checks/test_projects.sh index 5f9aafe80e10e..56f15f6839e9d 100755 --- a/test/scripts/checks/test_projects.sh +++ b/test/scripts/checks/test_projects.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:test_projects +checks-reporter-with-killswitch "Test Projects" \ + yarn kbn run test --exclude kibana --oss --skip-kibana-plugins diff --git a/test/scripts/checks/ts_projects.sh b/test/scripts/checks/ts_projects.sh index d667c753baec2..467beb2977efc 100755 --- a/test/scripts/checks/ts_projects.sh +++ b/test/scripts/checks/ts_projects.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:checkTsProjects +checks-reporter-with-killswitch "Check TypeScript Projects" \ + node scripts/check_ts_projects diff --git a/test/scripts/checks/type_check.sh b/test/scripts/checks/type_check.sh index 07c49638134be..5e091625de4ed 100755 --- a/test/scripts/checks/type_check.sh +++ b/test/scripts/checks/type_check.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:typeCheck +checks-reporter-with-killswitch "Check Types" \ + node scripts/type_check diff --git a/test/scripts/checks/verify_notice.sh b/test/scripts/checks/verify_notice.sh index 9f8343e540861..99bfd55edd3c1 100755 --- a/test/scripts/checks/verify_notice.sh +++ b/test/scripts/checks/verify_notice.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:verifyNotice +checks-reporter-with-killswitch "Verify NOTICE" \ + node scripts/notice --validate diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index f9e9d40cd8b0d..4faf645975c77 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -13,9 +13,9 @@ if [[ -z "$CODE_COVERAGE" ]]; then if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then source test/scripts/jenkins_build_kbn_sample_panel_action.sh - yarn run grunt run:pluginFunctionalTestsRelease --from=source; - yarn run grunt run:exampleFunctionalTestsRelease --from=source; - yarn run grunt run:interpreterFunctionalTestsRelease; + ./test/scripts/test/plugin_functional.sh + ./test/scripts/test/example_functional.sh + ./test/scripts/test/interpreter_functional.sh fi else echo " -> Running Functional tests with code coverage" diff --git a/test/scripts/jenkins_docs.sh b/test/scripts/jenkins_docs.sh index bd606d60101d8..f447afda1f948 100755 --- a/test/scripts/jenkins_docs.sh +++ b/test/scripts/jenkins_docs.sh @@ -3,4 +3,4 @@ set -e source "$(dirname $0)/../../src/dev/ci_setup/setup.sh" -"$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:docs; +"$(FORCE_COLOR=0 yarn bin)/grunt" docker:docs; diff --git a/test/scripts/jenkins_plugin_functional.sh b/test/scripts/jenkins_plugin_functional.sh index 1d691d98982de..1811bdeb4ed4b 100755 --- a/test/scripts/jenkins_plugin_functional.sh +++ b/test/scripts/jenkins_plugin_functional.sh @@ -10,6 +10,6 @@ cd -; pwd -yarn run grunt run:pluginFunctionalTestsRelease --from=source; -yarn run grunt run:exampleFunctionalTestsRelease --from=source; -yarn run grunt run:interpreterFunctionalTestsRelease; +./test/scripts/test/plugin_functional.sh +./test/scripts/test/example_functional.sh +./test/scripts/test/interpreter_functional.sh diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 1f6a3d440734b..c788a4a5b01ae 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -9,20 +9,43 @@ rename_coverage_file() { } if [[ -z "$CODE_COVERAGE" ]] ; then - "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --dev; + # Lint + ./test/scripts/lint/eslint.sh + ./test/scripts/lint/sasslint.sh + + # Test + ./test/scripts/test/jest_integration.sh + ./test/scripts/test/mocha.sh + ./test/scripts/test/jest_unit.sh + ./test/scripts/test/api_integration.sh + + # Check + ./test/scripts/checks/telemetry.sh + ./test/scripts/checks/ts_projects.sh + ./test/scripts/checks/jest_configs.sh + ./test/scripts/checks/doc_api_changes.sh + ./test/scripts/checks/type_check.sh + ./test/scripts/checks/bundle_limits.sh + ./test/scripts/checks/i18n.sh + ./test/scripts/checks/file_casing.sh + ./test/scripts/checks/licenses.sh + ./test/scripts/checks/plugins_with_circular_deps.sh + ./test/scripts/checks/verify_notice.sh + ./test/scripts/checks/test_projects.sh + ./test/scripts/checks/test_hardening.sh else - echo " -> Running jest tests with coverage" - node scripts/jest --ci --verbose --coverage - rename_coverage_file "oss" - echo "" - echo "" - echo " -> Running jest integration tests with coverage" - node --max-old-space-size=8192 scripts/jest_integration --ci --verbose --coverage || true; - rename_coverage_file "oss-integration" - echo "" - echo "" + # echo " -> Running jest tests with coverage" + # node scripts/jest --ci --verbose --coverage + # rename_coverage_file "oss" + # echo "" + # echo "" + # echo " -> Running jest integration tests with coverage" + # node --max-old-space-size=8192 scripts/jest_integration --ci --verbose --coverage || true; + # rename_coverage_file "oss-integration" + # echo "" + # echo "" echo " -> Running mocha tests with coverage" - yarn run grunt "test:mochaCoverage"; + ./test/scripts/checks/mocha_coverage.sh echo "" echo "" fi diff --git a/test/scripts/lint/eslint.sh b/test/scripts/lint/eslint.sh index c3211300b96c5..053150e42f409 100755 --- a/test/scripts/lint/eslint.sh +++ b/test/scripts/lint/eslint.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:eslint +checks-reporter-with-killswitch "Lint: eslint" \ + node scripts/eslint --no-cache diff --git a/test/scripts/lint/sasslint.sh b/test/scripts/lint/sasslint.sh index b9c683bcb049e..72e341cdcda16 100755 --- a/test/scripts/lint/sasslint.sh +++ b/test/scripts/lint/sasslint.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:sasslint +checks-reporter-with-killswitch "Lint: sasslint" \ + node scripts/sasslint diff --git a/test/scripts/server_integration.sh b/test/scripts/server_integration.sh deleted file mode 100755 index 82bc733e51b26..0000000000000 --- a/test/scripts/server_integration.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source test/scripts/jenkins_test_setup_oss.sh - -yarn run grunt run:serverIntegrationTests diff --git a/test/scripts/test/api_integration.sh b/test/scripts/test/api_integration.sh index 152c97a3ca7df..bf6f683989fe5 100755 --- a/test/scripts/test/api_integration.sh +++ b/test/scripts/test/api_integration.sh @@ -2,4 +2,8 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:apiIntegrationTests +checks-reporter-with-killswitch "API Integration Tests" \ + node scripts/functional_tests \ + --config test/api_integration/config.js \ + --bail \ + --debug diff --git a/test/scripts/test/example_functional.sh b/test/scripts/test/example_functional.sh new file mode 100755 index 0000000000000..08915085505bc --- /dev/null +++ b/test/scripts/test/example_functional.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +checks-reporter-with-killswitch "Example Functional Tests" \ + node scripts/functional_tests \ + --config test/examples/config.js \ + --bail \ + --debug diff --git a/test/scripts/test/interpreter_functional.sh b/test/scripts/test/interpreter_functional.sh new file mode 100755 index 0000000000000..1558989c0fdfc --- /dev/null +++ b/test/scripts/test/interpreter_functional.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +checks-reporter-with-killswitch "Interpreter Functional Tests" \ + node scripts/functional_tests \ + --config test/interpreter_functional/config.ts \ + --bail \ + --debug \ + --kibana-install-dir $KIBANA_INSTALL_DIR diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh index 73dbbddfb38f6..8791248e9a166 100755 --- a/test/scripts/test/jest_integration.sh +++ b/test/scripts/test/jest_integration.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:test_jest_integration +checks-reporter-with-killswitch "Jest Integration Tests" \ + node scripts/jest_integration diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index e25452698cebc..de5e16c2b1366 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:test_jest +checks-reporter-with-killswitch "Jest Unit Tests" \ + node scripts/jest diff --git a/test/scripts/test/karma_ci.sh b/test/scripts/test/karma_ci.sh deleted file mode 100755 index e9985300ba19d..0000000000000 --- a/test/scripts/test/karma_ci.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:test_karma_ci diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh index 43c00f0a09dcf..e5f3259926e42 100755 --- a/test/scripts/test/mocha.sh +++ b/test/scripts/test/mocha.sh @@ -2,4 +2,5 @@ source src/dev/ci_setup/setup_env.sh -yarn run grunt run:mocha +checks-reporter-with-killswitch "Mocha Tests" \ + node scripts/mocha diff --git a/test/scripts/test/plugin_functional.sh b/test/scripts/test/plugin_functional.sh new file mode 100755 index 0000000000000..e0af062e1de4a --- /dev/null +++ b/test/scripts/test/plugin_functional.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +checks-reporter-with-killswitch "Plugin Functional Tests" \ + node scripts/functional_tests \ + --config test/plugin_functional/config.ts \ + --bail \ + --debug diff --git a/test/scripts/test/server_integration.sh b/test/scripts/test/server_integration.sh new file mode 100755 index 0000000000000..1ff4a772bb6e0 --- /dev/null +++ b/test/scripts/test/server_integration.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +checks-reporter-with-killswitch "Server Integration Tests" \ + node scripts/functional_tests \ + --config test/server_integration/http/ssl/config.js \ + --config test/server_integration/http/ssl_redirect/config.js \ + --config test/server_integration/http/platform/config.ts \ + --config test/server_integration/http/ssl_with_p12/config.js \ + --config test/server_integration/http/ssl_with_p12_intermediate/config.js \ + --bail \ + --debug \ + --kibana-install-dir $KIBANA_INSTALL_DIR diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 521672e4bf48c..422a6c188979d 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -59,7 +59,7 @@ def uploadBaseWebsiteFiles(prefix) { def uploadCoverageHtmls(prefix) { [ 'target/kibana-coverage/functional-combined', - 'target/kibana-coverage/jest-combined', + // 'target/kibana-coverage/jest-combined', skipped due to failures 'target/kibana-coverage/mocha-combined', ].each { uploadWithVault(prefix, it) } } @@ -200,13 +200,14 @@ def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, teamAssignmen def runTests() { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() - } - }, + // skipping due to failures + // 'x-pack-intake-agent': { + // withEnv([ + // 'NODE_ENV=test' // Needed for jest tests only + // ]) { + // workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() + // } + // }, 'kibana-oss-agent' : workers.functional( 'kibana-oss-tests', { kibanaPipeline.buildOss() }, diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 348da83cc1364..22f446eeb00da 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -75,7 +75,7 @@ def functionalOss(Map params = [:]) { } if (config.serverIntegration) { - task(kibanaPipeline.scriptTaskDocker('serverIntegration', './test/scripts/server_integration.sh')) + task(kibanaPipeline.scriptTaskDocker('serverIntegration', './test/scripts/test/server_integration.sh')) } } } diff --git a/x-pack/plugins/alerts/common/disabled_action_groups.test.ts b/x-pack/plugins/alerts/common/disabled_action_groups.test.ts new file mode 100644 index 0000000000000..96db7bfd8710d --- /dev/null +++ b/x-pack/plugins/alerts/common/disabled_action_groups.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { isActionGroupDisabledForActionTypeId } from './disabled_action_groups'; +import { RecoveredActionGroup } from './builtin_action_groups'; + +test('returns false if action group id has no disabled types', () => { + expect(isActionGroupDisabledForActionTypeId('enabledActionGroup', '.jira')).toBeFalsy(); +}); + +test('returns false if action group id does not contains type', () => { + expect(isActionGroupDisabledForActionTypeId(RecoveredActionGroup.id, '.email')).toBeFalsy(); +}); + +test('returns true if action group id does contain type', () => { + expect(isActionGroupDisabledForActionTypeId(RecoveredActionGroup.id, '.jira')).toBeTruthy(); +}); diff --git a/x-pack/plugins/alerts/common/disabled_action_groups.ts b/x-pack/plugins/alerts/common/disabled_action_groups.ts new file mode 100644 index 0000000000000..525a267a278ea --- /dev/null +++ b/x-pack/plugins/alerts/common/disabled_action_groups.ts @@ -0,0 +1,24 @@ +/* + * 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 { RecoveredActionGroup } from './builtin_action_groups'; + +const DisabledActionGroupsByActionType: Record = { + [RecoveredActionGroup.id]: ['.jira', '.servicenow', '.resilient'], +}; + +export const DisabledActionTypeIdsForActionGroup: Map = new Map( + Object.entries(DisabledActionGroupsByActionType) +); + +export function isActionGroupDisabledForActionTypeId( + actionGroup: string, + actionTypeId: string +): boolean { + return ( + DisabledActionTypeIdsForActionGroup.has(actionGroup) && + DisabledActionTypeIdsForActionGroup.get(actionGroup)!.includes(actionTypeId) + ); +} diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts index 4d0e7bf7eb0bc..3e551facd98a0 100644 --- a/x-pack/plugins/alerts/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -13,6 +13,7 @@ export * from './alert_task_instance'; export * from './alert_navigation'; export * from './alert_instance_summary'; export * from './builtin_action_groups'; +export * from './disabled_action_groups'; export interface AlertingFrameworkHealth { isSufficientlySecure: boolean; 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 49a90c62bc581..93a479eeef487 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -33,6 +33,7 @@ const savedObjectsService = savedObjectsServiceMock.createInternalStartContract( const features = featuresPluginMock.createStart(); const securityPluginSetup = securityMock.createSetup(); +const securityPluginStart = securityMock.createStart(); const alertsClientFactoryParams: jest.Mocked = { logger: loggingSystemMock.create().get(), taskManager: taskManagerMock.createStart(), @@ -77,7 +78,7 @@ beforeEach(() => { test('creates an alerts client with proper constructor arguments when security is enabled', async () => { const factory = new AlertsClientFactory(); - factory.initialize({ securityPluginSetup, ...alertsClientFactoryParams }); + factory.initialize({ securityPluginSetup, securityPluginStart, ...alertsClientFactoryParams }); const request = KibanaRequest.from(fakeRequest); const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); @@ -98,7 +99,7 @@ test('creates an alerts client with proper constructor arguments when security i const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); expect(AlertsAuthorization).toHaveBeenCalledWith({ request, - authorization: securityPluginSetup.authz, + authorization: securityPluginStart.authz, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, features: alertsClientFactoryParams.features, auditLogger: expect.any(AlertsAuthorizationAuditLogger), @@ -188,11 +189,12 @@ test('getUserName() returns a name when security is enabled', async () => { factory.initialize({ ...alertsClientFactoryParams, securityPluginSetup, + securityPluginStart, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({ + securityPluginStart.authc.getCurrentUser.mockReturnValueOnce(({ username: 'bob', } as unknown) as AuthenticatedUser); const userNameResult = await constructorCall.getUserName(); @@ -225,7 +227,7 @@ test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce(null); + securityPluginStart.authc.apiKeys.grantAsInternalUser.mockResolvedValueOnce(null); const createAPIKeyResult = await constructorCall.createAPIKey(); expect(createAPIKeyResult).toEqual({ apiKeysEnabled: false }); }); @@ -235,11 +237,12 @@ test('createAPIKey() returns an API key when security is enabled', async () => { factory.initialize({ ...alertsClientFactoryParams, securityPluginSetup, + securityPluginStart, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({ + securityPluginStart.authc.apiKeys.grantAsInternalUser.mockResolvedValueOnce({ api_key: '123', id: 'abc', name: '', @@ -256,11 +259,12 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error', factory.initialize({ ...alertsClientFactoryParams, securityPluginSetup, + securityPluginStart, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockRejectedValueOnce( + securityPluginStart.authc.apiKeys.grantAsInternalUser.mockRejectedValueOnce( new Error('TLS disabled') ); await expect(constructorCall.createAPIKey()).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 9d71b5f817b2c..86091c89b6031 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -14,7 +14,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions import { AlertsClient } from './alerts_client'; import { ALERTS_FEATURE_ID } from '../common'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; -import { SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; @@ -28,6 +28,7 @@ export interface AlertsClientFactoryOpts { taskManager: TaskManagerStartContract; alertTypeRegistry: AlertTypeRegistry; securityPluginSetup?: SecurityPluginSetup; + securityPluginStart?: SecurityPluginStart; getSpaceId: (request: KibanaRequest) => string | undefined; getSpace: (request: KibanaRequest) => Promise; spaceIdToNamespace: SpaceIdToNamespaceFunction; @@ -44,6 +45,7 @@ export class AlertsClientFactory { private taskManager!: TaskManagerStartContract; private alertTypeRegistry!: AlertTypeRegistry; private securityPluginSetup?: SecurityPluginSetup; + private securityPluginStart?: SecurityPluginStart; private getSpaceId!: (request: KibanaRequest) => string | undefined; private getSpace!: (request: KibanaRequest) => Promise; private spaceIdToNamespace!: SpaceIdToNamespaceFunction; @@ -64,6 +66,7 @@ export class AlertsClientFactory { this.taskManager = options.taskManager; this.alertTypeRegistry = options.alertTypeRegistry; this.securityPluginSetup = options.securityPluginSetup; + this.securityPluginStart = options.securityPluginStart; this.spaceIdToNamespace = options.spaceIdToNamespace; this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient; this.actions = options.actions; @@ -73,10 +76,10 @@ export class AlertsClientFactory { } public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient { - const { securityPluginSetup, actions, eventLog, features } = this; + const { securityPluginSetup, securityPluginStart, actions, eventLog, features } = this; const spaceId = this.getSpaceId(request); const authorization = new AlertsAuthorization({ - authorization: securityPluginSetup?.authz, + authorization: securityPluginStart?.authz, request, getSpace: this.getSpace, alertTypeRegistry: this.alertTypeRegistry, @@ -102,25 +105,22 @@ export class AlertsClientFactory { encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, auditLogger: securityPluginSetup?.audit.asScoped(request), async getUserName() { - if (!securityPluginSetup) { + if (!securityPluginStart) { return null; } - const user = await securityPluginSetup.authc.getCurrentUser(request); + const user = await securityPluginStart.authc.getCurrentUser(request); return user ? user.username : null; }, async createAPIKey(name: string) { - if (!securityPluginSetup) { + if (!securityPluginStart) { return { apiKeysEnabled: false }; } // Create an API key using the new grant API - in this case the Kibana system user is creating the // API key for the user, instead of having the user create it themselves, which requires api_key // privileges - const createAPIKeyResult = await securityPluginSetup.authc.grantAPIKeyAsInternalUser( + const createAPIKeyResult = await securityPluginStart.authc.apiKeys.grantAsInternalUser( request, - { - name, - role_descriptors: {}, - } + { name, role_descriptors: {} } ); if (!createAPIKeyResult) { return { apiKeysEnabled: false }; diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts index 119c3b697fd2e..91c3f5954d6d0 100644 --- a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts @@ -12,7 +12,7 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; -import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../security/server'; +import { InvalidateAPIKeyParams, SecurityPluginStart } from '../../../security/server'; import { RunContext, TaskManagerSetupContract, @@ -29,12 +29,12 @@ export const TASK_ID = `Alerts-${TASK_TYPE}`; const invalidateAPIKey = async ( params: InvalidateAPIKeyParams, - securityPluginSetup?: SecurityPluginSetup + securityPluginStart?: SecurityPluginStart ): Promise => { - if (!securityPluginSetup) { + if (!securityPluginStart) { return { apiKeysEnabled: false }; } - const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser( + const invalidateAPIKeyResult = await securityPluginStart.authc.apiKeys.invalidateAsInternalUser( params ); // Null when Elasticsearch security is disabled @@ -51,16 +51,9 @@ export function initializeApiKeyInvalidator( logger: Logger, coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, taskManager: TaskManagerSetupContract, - config: Promise, - securityPluginSetup?: SecurityPluginSetup + config: Promise ) { - registerApiKeyInvalitorTaskDefinition( - logger, - coreStartServices, - taskManager, - config, - securityPluginSetup - ); + registerApiKeyInvalidatorTaskDefinition(logger, coreStartServices, taskManager, config); } export async function scheduleApiKeyInvalidatorTask( @@ -84,17 +77,16 @@ export async function scheduleApiKeyInvalidatorTask( } } -function registerApiKeyInvalitorTaskDefinition( +function registerApiKeyInvalidatorTaskDefinition( logger: Logger, coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, taskManager: TaskManagerSetupContract, - config: Promise, - securityPluginSetup?: SecurityPluginSetup + config: Promise ) { taskManager.registerTaskDefinitions({ [TASK_TYPE]: { title: 'Invalidate alert API Keys', - createTaskRunner: taskRunner(logger, coreStartServices, config, securityPluginSetup), + createTaskRunner: taskRunner(logger, coreStartServices, config), }, }); } @@ -120,8 +112,7 @@ function getFakeKibanaRequest(basePath: string) { function taskRunner( logger: Logger, coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, - config: Promise, - securityPluginSetup?: SecurityPluginSetup + config: Promise ) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; @@ -130,7 +121,10 @@ function taskRunner( let totalInvalidated = 0; const configResult = await config; try { - const [{ savedObjects, http }, { encryptedSavedObjects }] = await coreStartServices; + const [ + { savedObjects, http }, + { encryptedSavedObjects, security }, + ] = await coreStartServices; const savedObjectsClient = savedObjects.getScopedClient( getFakeKibanaRequest(http.basePath.serverBasePath), { @@ -160,7 +154,7 @@ function taskRunner( savedObjectsClient, apiKeysToInvalidate, encryptedSavedObjectsClient, - securityPluginSetup + security ); hasApiKeysPendingInvalidation = apiKeysToInvalidate.total > PAGE_SIZE; @@ -197,7 +191,7 @@ async function invalidateApiKeys( savedObjectsClient: SavedObjectsClientContract, apiKeysToInvalidate: SavedObjectsFindResponse, encryptedSavedObjectsClient: EncryptedSavedObjectsClient, - securityPluginSetup?: SecurityPluginSetup + securityPluginStart?: SecurityPluginStart ) { let totalInvalidated = 0; await Promise.all( @@ -207,7 +201,7 @@ async function invalidateApiKeys( apiKeyObj.id ); const apiKeyId = decryptedApiKey.attributes.apiKeyId; - const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginSetup); + const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginStart); if (response.apiKeysEnabled === true && response.result.error_count > 0) { logger.error(`Failed to invalidate API Key [id="${apiKeyObj.attributes.apiKeyId}"]`); } else { diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index bafb89c64076b..e526c65b90102 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -8,7 +8,7 @@ import { first, map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { combineLatest } from 'rxjs'; -import { SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart, @@ -115,6 +115,7 @@ export interface AlertingPluginsStart { features: FeaturesPluginStart; eventLog: IEventLogClientService; spaces?: SpacesPluginStart; + security?: SecurityPluginStart; } export class AlertingPlugin { @@ -203,8 +204,7 @@ export class AlertingPlugin { this.logger, core.getStartServices(), plugins.taskManager, - this.config, - this.security + this.config ); core.getStartServices().then(async ([, startPlugins]) => { @@ -279,6 +279,7 @@ export class AlertingPlugin { logger, taskManager: plugins.taskManager, securityPluginSetup: security, + securityPluginStart: plugins.security, encryptedSavedObjectsClient, spaceIdToNamespace, getSpaceId(request: KibanaRequest) { diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 8c233d3691c7f..c15fb7110c473 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -118,6 +118,10 @@ exports[`Error SPAN_ACTION 1`] = `undefined`; exports[`Error SPAN_DESTINATION_SERVICE_RESOURCE 1`] = `undefined`; +exports[`Error SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT 1`] = `undefined`; + +exports[`Error SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM 1`] = `undefined`; + exports[`Error SPAN_DURATION 1`] = `undefined`; exports[`Error SPAN_ID 1`] = `undefined`; @@ -290,6 +294,10 @@ exports[`Span SPAN_ACTION 1`] = `"my action"`; exports[`Span SPAN_DESTINATION_SERVICE_RESOURCE 1`] = `undefined`; +exports[`Span SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT 1`] = `undefined`; + +exports[`Span SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM 1`] = `undefined`; + exports[`Span SPAN_DURATION 1`] = `1337`; exports[`Span SPAN_ID 1`] = `"span id"`; @@ -462,6 +470,10 @@ exports[`Transaction SPAN_ACTION 1`] = `undefined`; exports[`Transaction SPAN_DESTINATION_SERVICE_RESOURCE 1`] = `undefined`; +exports[`Transaction SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT 1`] = `undefined`; + +exports[`Transaction SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM 1`] = `undefined`; + exports[`Transaction SPAN_DURATION 1`] = `undefined`; exports[`Transaction SPAN_ID 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index 43b3748231290..e978b6d55251b 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -110,7 +110,7 @@ export const generalSettings: RawSettingDefinition[] = [ { text: 'critical', value: 'critical' }, { text: 'off', value: 'off' }, ], - includeAgents: ['dotnet', 'ruby', 'java', 'python'], + includeAgents: ['dotnet', 'ruby', 'java', 'python', 'nodejs'], }, // Recording @@ -235,7 +235,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'Sometimes it is necessary to sanitize, i.e., remove, sensitive data sent to Elastic APM. This config accepts a list of wildcard patterns of field names which should be sanitized. These apply to HTTP headers (including cookies) and `application/x-www-form-urlencoded` data (POST form fields). The query string and the captured request body (such as `application/json` data) will not get sanitized.', } ), - includeAgents: ['java', 'python'], + includeAgents: ['java', 'python', 'go'], }, // Ignore transactions based on URLs diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index c9637f20a51bc..abe353ab8f3a3 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -46,6 +46,7 @@ describe('filterByAgent', () => { 'capture_body', 'capture_headers', 'recording', + 'sanitize_field_names', 'span_frames_min_duration', 'stack_trace_limit', 'transaction_max_spans', @@ -100,6 +101,7 @@ describe('filterByAgent', () => { it('nodejs', () => { expect(getSettingKeysForAgent('nodejs')).toEqual([ 'capture_body', + 'log_level', 'transaction_max_spans', 'transaction_sample_rate', ]); diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index cc6a1fffb2288..18b8dc57c88db 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -61,6 +61,11 @@ export const SPAN_NAME = 'span.name'; export const SPAN_ID = 'span.id'; export const SPAN_DESTINATION_SERVICE_RESOURCE = 'span.destination.service.resource'; +export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT = + 'span.destination.service.response_time.count'; + +export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM = + 'span.destination.service.response_time.sum.us'; // Parent ID for a transaction or span export const PARENT_ID = 'parent.id'; diff --git a/x-pack/plugins/apm/common/utils/formatters/duration.ts b/x-pack/plugins/apm/common/utils/formatters/duration.ts index 8e563399a0f1f..cf2d99fe5119d 100644 --- a/x-pack/plugins/apm/common/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/common/utils/formatters/duration.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { memoize } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; -import { asDecimal, asDecimalOrInteger, asInteger } from './formatters'; +import { asDecimalOrInteger, asInteger, asDecimal } from './formatters'; import { TimeUnit } from './datetime'; import { Maybe } from '../../../typings/common'; import { isFiniteNumber } from '../is_finite_number'; @@ -181,7 +181,6 @@ export function asDuration( const formatter = getDurationFormatter(value); return formatter(value, { defaultValue }).formatted; } - /** * Convert a microsecond value to decimal milliseconds. Normally we use * `asDuration`, but this is used in places like tables where we always want diff --git a/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts b/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts index 458d21bfea58f..59109c720e9c9 100644 --- a/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts +++ b/x-pack/plugins/apm/common/utils/join_by_key/index.test.ts @@ -101,4 +101,65 @@ describe('joinByKey', () => { }, ]); }); + + it('uses the custom merge fn to replace items', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-java', + values: ['a'], + }, + { + serviceName: 'opbeans-node', + values: ['a'], + }, + { + serviceName: 'opbeans-node', + values: ['b'], + }, + { + serviceName: 'opbeans-node', + values: ['c'], + }, + ], + 'serviceName', + (a, b) => ({ + ...a, + ...b, + values: a.values.concat(b.values), + }) + ); + + expect( + joined.find((item) => item.serviceName === 'opbeans-node')?.values + ).toEqual(['a', 'b', 'c']); + }); + + it('deeply merges objects', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + properties: { + foo: '', + }, + }, + { + serviceName: 'opbeans-node', + properties: { + bar: '', + }, + }, + ], + 'serviceName' + ); + + expect(joined[0]).toEqual({ + serviceName: 'opbeans-node', + properties: { + foo: '', + bar: '', + }, + }); + }); }); diff --git a/x-pack/plugins/apm/common/utils/join_by_key/index.ts b/x-pack/plugins/apm/common/utils/join_by_key/index.ts index b49f536400514..6678bf68afbae 100644 --- a/x-pack/plugins/apm/common/utils/join_by_key/index.ts +++ b/x-pack/plugins/apm/common/utils/join_by_key/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { UnionToIntersection, ValuesType } from 'utility-types'; -import { isEqual } from 'lodash'; +import { isEqual, pull, merge, castArray } from 'lodash'; /** * Joins a list of records by a given key. Key can be any type of value, from @@ -23,24 +23,48 @@ import { isEqual } from 'lodash'; */ type JoinedReturnType< + T extends Record, + U extends UnionToIntersection +> = Array< + Partial & + { + [k in keyof T]: T[k]; + } +>; + +type ArrayOrSingle = T | T[]; + +export function joinByKey< T extends Record, U extends UnionToIntersection, - V extends keyof T & keyof U -> = Array & Record>; + V extends ArrayOrSingle +>(items: T[], key: V): JoinedReturnType; export function joinByKey< T extends Record, U extends UnionToIntersection, - V extends keyof T & keyof U ->(items: T[], key: V): JoinedReturnType { - return items.reduce>((prev, current) => { - let item = prev.find((prevItem) => isEqual(prevItem[key], current[key])); + V extends ArrayOrSingle, + W extends JoinedReturnType, + X extends (a: T, b: T) => ValuesType +>(items: T[], key: V, mergeFn: X): W; + +export function joinByKey( + items: Array>, + key: string | string[], + mergeFn: Function = (a: Record, b: Record) => + merge({}, a, b) +) { + const keys = castArray(key); + return items.reduce>>((prev, current) => { + let item = prev.find((prevItem) => + keys.every((k) => isEqual(prevItem[k], current[k])) + ); if (!item) { - item = { ...current } as ValuesType>; + item = { ...current }; prev.push(item); } else { - Object.assign(item, current); + pull(prev, item).push(mergeFn(item, current)); } return prev; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts index f2f51496fcca8..9a0ebb7173c26 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape_options.ts @@ -16,7 +16,7 @@ import { ServiceHealthStatus, } from '../../../../common/service_health_status'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { defaultIcon, iconForNode } from './icons'; +import { iconForNode } from './icons'; export const popoverWidth = 280; @@ -116,9 +116,7 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { 'background-color': theme.eui.euiColorGhost, // The DefinitelyTyped definitions don't specify that a function can be // used here. - 'background-image': isIE11 - ? undefined - : (el: cytoscape.NodeSingular) => iconForNode(el) ?? defaultIcon, + 'background-image': (el: cytoscape.NodeSingular) => iconForNode(el), 'background-height': (el: cytoscape.NodeSingular) => isService(el) ? '60%' : '40%', 'background-width': (el: cytoscape.NodeSingular) => diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts index c85cf85d38702..e64c84f130c46 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -10,73 +10,8 @@ import { SPAN_SUBTYPE, SPAN_TYPE, } from '../../../../common/elasticsearch_fieldnames'; -import awsIcon from './icons/aws.svg'; -import cassandraIcon from './icons/cassandra.svg'; -import databaseIcon from './icons/database.svg'; -import defaultIconImport from './icons/default.svg'; -import documentsIcon from './icons/documents.svg'; -import elasticsearchIcon from './icons/elasticsearch.svg'; -import globeIcon from './icons/globe.svg'; -import graphqlIcon from './icons/graphql.svg'; -import grpcIcon from './icons/grpc.svg'; -import handlebarsIcon from './icons/handlebars.svg'; -import kafkaIcon from './icons/kafka.svg'; -import mongodbIcon from './icons/mongodb.svg'; -import mysqlIcon from './icons/mysql.svg'; -import postgresqlIcon from './icons/postgresql.svg'; -import redisIcon from './icons/redis.svg'; -import websocketIcon from './icons/websocket.svg'; -import javaIcon from '../../shared/AgentIcon/icons/java.svg'; import { getAgentIcon } from '../../shared/AgentIcon/get_agent_icon'; - -export const defaultIcon = defaultIconImport; - -const defaultTypeIcons: { [key: string]: string } = { - cache: databaseIcon, - db: databaseIcon, - ext: globeIcon, - external: globeIcon, - messaging: documentsIcon, - resource: globeIcon, -}; - -const typeIcons: { [key: string]: { [key: string]: string } } = { - aws: { - servicename: awsIcon, - }, - db: { - cassandra: cassandraIcon, - elasticsearch: elasticsearchIcon, - mongodb: mongodbIcon, - mysql: mysqlIcon, - postgresql: postgresqlIcon, - redis: redisIcon, - }, - external: { - graphql: graphqlIcon, - grpc: grpcIcon, - websocket: websocketIcon, - }, - messaging: { - jms: javaIcon, - kafka: kafkaIcon, - }, - template: { - handlebars: handlebarsIcon, - }, -}; - -function getSpanIcon(type?: string, subtype?: string) { - if (!type) { - return; - } - - const types = type ? typeIcons[type] : {}; - if (subtype && types && subtype in types) { - return types[subtype]; - } - return defaultTypeIcons[type] || defaultIcon; -} +import { defaultIcon, getSpanIcon } from '../../shared/span_icon/get_span_icon'; // IE 11 does not properly load some SVGs, which causes a runtime error and the // map to not work at all. We would prefer to do some kind of feature detection diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index c491b9f0e1eff..6810b56fb8f87 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -17,7 +17,6 @@ import React, { useMemo } from 'react'; import { isEmpty, flatten } from 'lodash'; import { useHistory } from 'react-router-dom'; import { RouteComponentProps } from 'react-router-dom'; -import { useTransactionChartsFetcher } from '../../../hooks/use_transaction_charts_fetcher'; import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher'; import { useWaterfallFetcher } from './use_waterfall_fetcher'; import { ApmHeader } from '../../shared/ApmHeader'; @@ -54,11 +53,6 @@ export function TransactionDetails({ distributionStatus, } = useTransactionDistributionFetcher(); - const { - transactionChartsData, - transactionChartsStatus, - } = useTransactionChartsFetcher(); - const { waterfall, exceedsMax, @@ -128,11 +122,7 @@ export function TransactionDetails({ - + diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index dcb407d27e690..1f6a9276b5d27 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -18,12 +18,11 @@ import { isRumAgentName } from '../../../../common/agent_name'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; -import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { SearchBar } from '../../shared/search_bar'; import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; +import { ServiceOverviewDependenciesTable } from './service_overview_dependencies_table'; import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart'; import { ServiceOverviewTransactionsTable } from './service_overview_transactions_table'; -import { TableLinkFlexItem } from './table_link_flex_item'; /** * The height a chart should be if it's next to a table with 5 rows and a title. @@ -98,30 +97,7 @@ export function ServiceOverview({ - - - -

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

- - - - - {i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableLinkText', - { - defaultMessage: 'View service map', - } - )} - - - + 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 index 5b05497b482ce..3db857ad32190 100644 --- 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 @@ -15,12 +15,13 @@ import { } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; -import * as useFetcherHooks from '../../../hooks/use_fetcher'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import * as useAnnotationsHooks from '../../../context/annotations/use_annotations_context'; import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_breakdown_chart/use_transaction_breakdown'; import { renderWithTheme } from '../../../utils/testHelpers'; import { ServiceOverview } from './'; +import { waitFor } from '@testing-library/dom'; +import * as callApmApi from '../../../services/rest/createCallApmApi'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -54,7 +55,7 @@ function Wrapper({ children }: { children?: ReactNode }) { } describe('ServiceOverview', () => { - it('renders', () => { + it('renders', async () => { jest .spyOn(useAnnotationsHooks, 'useAnnotationsContext') .mockReturnValue({ annotations: [] }); @@ -64,18 +65,29 @@ describe('ServiceOverview', () => { indexPattern: undefined, status: FETCH_STATUS.SUCCESS, }); - jest.spyOn(useFetcherHooks, 'useFetcher').mockReturnValue({ - data: { - items: [], - tableOptions: { - pageIndex: 0, - sort: { direction: 'desc', field: 'test field' }, - }, - totalItemCount: 0, - throughput: [], + + const calls = { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GET /api/apm/services/{serviceName}/error_groups': { + error_groups: [], + total_error_groups: 0, + }, + 'GET /api/apm/services/{serviceName}/transactions/groups/overview': { + transactionGroups: [], + totalTransactionGroups: 0, + isAggregationAccurate: true, }, - refetch: () => {}, - status: FETCH_STATUS.SUCCESS, + 'GET /api/apm/services/{serviceName}/dependencies': [], + }; + + jest.spyOn(callApmApi, 'createCallApmApi').mockImplementation(() => {}); + + jest.spyOn(callApmApi, 'callApmApi').mockImplementation(({ endpoint }) => { + const response = calls[endpoint as keyof typeof calls]; + + return response + ? Promise.resolve(response) + : Promise.reject(`Response for ${endpoint} is not defined`); }); jest .spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown') @@ -85,10 +97,19 @@ describe('ServiceOverview', () => { status: FETCH_STATUS.SUCCESS, }); - expect(() => - renderWithTheme(, { + const { findAllByText } = renderWithTheme( + , + { wrapper: Wrapper, - }) - ).not.toThrowError(); + } + ); + + await waitFor(() => + expect(callApmApi.callApmApi).toHaveBeenCalledTimes( + Object.keys(calls).length + ) + ); + + expect((await findAllByText('Latency')).length).toBeGreaterThan(0); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx new file mode 100644 index 0000000000000..87ff702e0a960 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -0,0 +1,249 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import { EuiInMemoryTable } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; +import { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + asDuration, + asPercent, + asTransactionRate, +} from '../../../../../common/utils/formatters'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceDependencyItem } from '../../../../../server/lib/services/get_service_dependencies'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { ServiceMapLink } from '../../../shared/Links/apm/ServiceMapLink'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { TableLinkFlexItem } from '../table_link_flex_item'; +import { AgentIcon } from '../../../shared/AgentIcon'; +import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { px, unit } from '../../../../style/variables'; +import { ImpactBar } from '../../../shared/ImpactBar'; +import { ServiceOverviewLink } from '../../../shared/Links/apm/service_overview_link'; +import { SpanIcon } from '../../../shared/span_icon'; +import { ServiceOverviewTableContainer } from '../service_overview_table'; + +interface Props { + serviceName: string; +} + +export function ServiceOverviewDependenciesTable({ serviceName }: Props) { + const columns: Array> = [ + { + field: 'name', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnBackend', + { + defaultMessage: 'Backend', + } + ), + render: (_, item) => { + return ( + + + {item.type === 'service' ? ( + + ) : ( + + )} + + + {item.type === 'service' ? ( + + {item.name} + + ) : ( + item.name + )} + + + } + /> + ); + }, + sortable: true, + }, + { + field: 'latencyValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnLatency', + { + defaultMessage: 'Latency', + } + ), + width: px(unit * 10), + render: (_, { latency }) => { + return ( + + ); + }, + sortable: true, + }, + { + field: 'throughputValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnThroughput', + { + defaultMessage: 'Traffic', + } + ), + width: px(unit * 10), + render: (_, { throughput }) => { + return ( + + ); + }, + sortable: true, + }, + { + field: 'errorRateValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnErrorRate', + { + defaultMessage: 'Error rate', + } + ), + width: px(unit * 10), + render: (_, { errorRate }) => { + return ( + + ); + }, + sortable: true, + }, + { + field: 'impactValue', + name: i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableColumnImpact', + { + defaultMessage: 'Impact', + } + ), + width: px(unit * 5), + render: (_, { impact }) => { + return ; + }, + sortable: true, + }, + ]; + + const { + urlParams: { start, end, environment }, + } = useUrlParams(); + + const { data = [], status } = useFetcher(() => { + if (!start || !end) { + return; + } + + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/dependencies', + params: { + path: { + serviceName, + }, + query: { + start, + end, + environment: environment || ENVIRONMENT_ALL.value, + numBuckets: 20, + }, + }, + }); + }, [start, end, serviceName, environment]); + + // need top-level sortable fields for the managed table + const items = data.map((item) => ({ + ...item, + errorRateValue: item.errorRate.value, + latencyValue: item.latency.value, + throughputValue: item.throughput.value, + impactValue: item.impact, + })); + + return ( + + + + + +

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

+
+
+ + + {i18n.translate( + 'xpack.apm.serviceOverview.dependenciesTableLinkText', + { + defaultMessage: 'View service map', + } + )} + + +
+
+ + + + + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table.tsx index b54458e4555f7..99753adfcd36d 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table.tsx @@ -21,7 +21,7 @@ const tableHeight = 298; * * Hide the empty message when we don't yet have any items and are still loading. */ -const ServiceOverviewTableContainer = styled.div<{ +export const ServiceOverviewTableContainer = styled.div<{ isEmptyAndLoading: boolean; }>` height: ${tableHeight}px; diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 9ff4ad916b174..ad2b68ae8a4ef 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -24,7 +24,6 @@ import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; import { IUrlParams } from '../../../context/url_params_context/types'; -import { useTransactionChartsFetcher } from '../../../hooks/use_transaction_charts_fetcher'; import { useTransactionListFetcher } from './use_transaction_list'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; @@ -73,11 +72,6 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { // redirect to first transaction type useRedirect(getRedirectLocation({ location, transactionType, urlParams })); - const { - transactionChartsData, - transactionChartsStatus, - } = useTransactionChartsFetcher(); - useTrackPageview({ app: 'apm', path: 'transaction_overview' }); useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 }); const { @@ -132,11 +126,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { )} - + diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 73a819af2d624..ab1e725a08dff 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -3,25 +3,23 @@ * 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 { - ScaleType, - Chart, - Settings, AreaSeries, + Chart, CurveType, + ScaleType, + Settings, } from '@elastic/charts'; -import { EuiIcon } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { px } from '../../../../style/variables'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; +import React from 'react'; +import { merge } from 'lodash'; import { useChartTheme } from '../../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +import { px } from '../../../../style/variables'; interface Props { color: string; - series?: Array<{ x: number; y: number | null }>; + series?: Array<{ x: number; y: number | null }> | null; width: string; } @@ -46,7 +44,18 @@ export function SparkPlot(props: Props) { return ( - + ; + series?: Array<{ x: number; y: number | null }> | null; valueLabel: React.ReactNode; compact?: boolean; }) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index 947a3a6e89bd1..afc8951f121ea 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -34,10 +34,10 @@ import { useTheme } from '../../../hooks/use_theme'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useAnnotationsContext } from '../../../context/annotations/use_annotations_context'; import { useChartPointerEventContext } from '../../../context/chart_pointer_event/use_chart_pointer_event_context'; -import { AnomalySeries } from '../../../selectors/chart_selectors'; import { unit } from '../../../style/variables'; import { ChartContainer } from './chart_container'; import { onBrushEnd } from './helper/helper'; +import { getLatencyChartSelector } from '../../../selectors/latency_chart_selectors'; interface Props { id: string; @@ -55,7 +55,9 @@ interface Props { yTickFormat?: (y: number) => string; showAnnotations?: boolean; yDomain?: YDomainRange; - anomalySeries?: AnomalySeries; + anomalySeries?: ReturnType< + typeof getLatencyChartSelector + >['anomalyTimeseries']; } export function TimeseriesChart({ diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index bb7c0a9104fc7..f43019a5101d0 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -23,9 +23,9 @@ import { asTransactionRate } from '../../../../../common/utils/formatters'; import { AnnotationsContextProvider } from '../../../../context/annotations/annotations_context'; import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context'; import { LicenseContext } from '../../../../context/license/license_context'; -import type { IUrlParams } from '../../../../context/url_params_context/types'; -import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { ITransactionChartData } from '../../../../selectors/chart_selectors'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { useTransactionLatencyChartsFetcher } from '../../../../hooks/use_transaction_latency_chart_fetcher'; +import { useTransactionThroughputChartsFetcher } from '../../../../hooks/use_transaction_throughput_chart_fetcher'; import { TimeseriesChart } from '../timeseries_chart'; import { TransactionBreakdownChart } from '../transaction_breakdown_chart'; import { TransactionErrorRateChart } from '../transaction_error_rate_chart/'; @@ -33,22 +33,24 @@ import { getResponseTimeTickFormatter } from './helper'; import { MLHeader } from './ml_header'; import { useFormatter } from './use_formatter'; -interface TransactionChartProps { - charts: ITransactionChartData; - urlParams: IUrlParams; - fetchStatus: FETCH_STATUS; -} - -export function TransactionCharts({ - charts, - urlParams, - fetchStatus, -}: TransactionChartProps) { +export function TransactionCharts() { + const { urlParams } = useUrlParams(); const { transactionType } = urlParams; - const { responseTimeSeries, tpmSeries, anomalySeries } = charts; + const { + latencyChartsData, + latencyChartsStatus, + } = useTransactionLatencyChartsFetcher(); + + const { + throughputChartsData, + throughputChartsStatus, + } = useTransactionThroughputChartsFetcher(); + + const { latencyTimeseries, anomalyTimeseries, mlJobId } = latencyChartsData; + const { throughputTimeseries } = throughputChartsData; - const { formatter, toggleSerie } = useFormatter(responseTimeSeries); + const { formatter, toggleSerie } = useFormatter(latencyTimeseries); return ( <> @@ -69,17 +71,17 @@ export function TransactionCharts({ hasValidMlLicense={ license?.getFeature('ml').isAvailable } - mlJobId={charts.mlJobId} + mlJobId={mlJobId} /> )} { if (serie) { toggleSerie(serie); @@ -95,9 +97,9 @@ export function TransactionCharts({ {tpmLabel(transactionType)} diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts b/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts new file mode 100644 index 0000000000000..d1062d1044ead --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/get_span_icon.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { maybe } from '../../../../common/utils/maybe'; +import awsIcon from './icons/aws.svg'; +import cassandraIcon from './icons/cassandra.svg'; +import databaseIcon from './icons/database.svg'; +import defaultIconImport from './icons/default.svg'; +import documentsIcon from './icons/documents.svg'; +import elasticsearchIcon from './icons/elasticsearch.svg'; +import globeIcon from './icons/globe.svg'; +import graphqlIcon from './icons/graphql.svg'; +import grpcIcon from './icons/grpc.svg'; +import handlebarsIcon from './icons/handlebars.svg'; +import kafkaIcon from './icons/kafka.svg'; +import mongodbIcon from './icons/mongodb.svg'; +import mysqlIcon from './icons/mysql.svg'; +import postgresqlIcon from './icons/postgresql.svg'; +import redisIcon from './icons/redis.svg'; +import websocketIcon from './icons/websocket.svg'; +import javaIcon from '../../shared/AgentIcon/icons/java.svg'; + +const defaultTypeIcons: { [key: string]: string } = { + cache: databaseIcon, + db: databaseIcon, + ext: globeIcon, + external: globeIcon, + messaging: documentsIcon, + resource: globeIcon, +}; + +const typeIcons: { [key: string]: { [key: string]: string } } = { + aws: { + servicename: awsIcon, + }, + db: { + cassandra: cassandraIcon, + elasticsearch: elasticsearchIcon, + mongodb: mongodbIcon, + mysql: mysqlIcon, + postgresql: postgresqlIcon, + redis: redisIcon, + }, + external: { + graphql: graphqlIcon, + grpc: grpcIcon, + websocket: websocketIcon, + }, + messaging: { + jms: javaIcon, + kafka: kafkaIcon, + }, + template: { + handlebars: handlebarsIcon, + }, +}; + +export const defaultIcon = defaultIconImport; + +export function getSpanIcon(type?: string, subtype?: string) { + if (!type) { + return defaultIcon; + } + + const types = maybe(typeIcons[type]); + + if (subtype && types && subtype in types) { + return types[subtype]; + } + return defaultTypeIcons[type] || defaultIcon; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/aws.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/aws.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/aws.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/aws.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/cassandra.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/cassandra.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/database.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/database.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/database.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/database.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/default.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/default.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/default.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/default.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/documents.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/documents.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/documents.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/documents.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/elasticsearch.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/elasticsearch.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/globe.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/globe.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/globe.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/globe.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/graphql.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/graphql.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/grpc.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/grpc.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/handlebars.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/handlebars.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/kafka.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/kafka.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/mongodb.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/mongodb.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/mysql.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/mysql.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/postgresql.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/postgresql.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/redis.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/redis.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/redis.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/redis.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg b/x-pack/plugins/apm/public/components/shared/span_icon/icons/websocket.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg rename to x-pack/plugins/apm/public/components/shared/span_icon/icons/websocket.svg diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx b/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx new file mode 100644 index 0000000000000..98b076db65513 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { px } from '../../../style/variables'; +import { getSpanIcon } from './get_span_icon'; + +interface Props { + type?: string; + subType?: string; +} + +export function SpanIcon({ type, subType }: Props) { + const icon = getSpanIcon(type, subType); + + return {type; +} diff --git a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx new file mode 100644 index 0000000000000..04c4e893577f9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { truncate } from '../../../style/variables'; + +const tooltipAnchorClassname = '_apm_truncate_tooltip_anchor_'; + +const TooltipWrapper = styled.div` + width: 100%; + .${tooltipAnchorClassname} { + width: 100% !important; + display: block !important; + } +`; + +const ContentWrapper = styled.div` + ${truncate('100%')} +`; + +interface Props { + text: string; + content?: React.ReactNode; +} + +export function TruncateWithTooltip(props: Props) { + const { text, content } = props; + + return ( + + + {content || text} + + + ); +} diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts similarity index 70% rename from x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts rename to x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts index 406a1a4633577..2434ec9c977ed 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_charts_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts @@ -6,12 +6,14 @@ import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { getTransactionCharts } from '../selectors/chart_selectors'; import { useFetcher } from './use_fetcher'; import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { getLatencyChartSelector } from '../selectors/latency_chart_selectors'; +import { useTheme } from './use_theme'; -export function useTransactionChartsFetcher() { +export function useTransactionLatencyChartsFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); + const theme = useTheme(); const { urlParams: { transactionType, start, end, transactionName }, uiFilters, @@ -21,7 +23,8 @@ export function useTransactionChartsFetcher() { (callApmApi) => { if (serviceName && start && end) { return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts', + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/charts/latency', params: { path: { serviceName }, query: { @@ -39,13 +42,13 @@ export function useTransactionChartsFetcher() { ); const memoizedData = useMemo( - () => getTransactionCharts({ transactionType }, data), - [data, transactionType] + () => getLatencyChartSelector({ latencyChart: data, theme }), + [data, theme] ); return { - transactionChartsData: memoizedData, - transactionChartsStatus: status, - transactionChartsError: error, + latencyChartsData: memoizedData, + latencyChartsStatus: status, + latencyChartsError: error, }; } diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts new file mode 100644 index 0000000000000..c03bb8efc79b3 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { useFetcher } from './use_fetcher'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { getThrouputChartSelector } from '../selectors/throuput_chart_selectors'; +import { useTheme } from './use_theme'; + +export function useTransactionThroughputChartsFetcher() { + const { serviceName } = useParams<{ serviceName?: string }>(); + const theme = useTheme(); + const { + urlParams: { transactionType, start, end, transactionName }, + uiFilters, + } = useUrlParams(); + + const { data, error, status } = useFetcher( + (callApmApi) => { + if (serviceName && start && end) { + return callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/charts/throughput', + params: { + path: { serviceName }, + query: { + start, + end, + transactionType, + transactionName, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, + [serviceName, start, end, transactionName, transactionType, uiFilters] + ); + + const memoizedData = useMemo( + () => getThrouputChartSelector({ throuputChart: data, theme }), + [data, theme] + ); + + return { + throughputChartsData: memoizedData, + throughputChartsStatus: status, + throughputChartsError: error, + }; +} diff --git a/x-pack/plugins/apm/public/selectors/chart_selectors.test.ts b/x-pack/plugins/apm/public/selectors/chart_selectors.test.ts deleted file mode 100644 index c9e6177f2c721..0000000000000 --- a/x-pack/plugins/apm/public/selectors/chart_selectors.test.ts +++ /dev/null @@ -1,269 +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 theme from '@elastic/eui/dist/eui_theme_light.json'; -import { - getAnomalyScoreSeries, - getResponseTimeSeries, - getTpmSeries, -} from './chart_selectors'; -import { - successColor, - warningColor, - errorColor, -} from '../utils/httpStatusCodeToColor'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ApmTimeSeriesResponse } from '../../server/lib/transactions/charts/get_timeseries_data/transform'; - -describe('chart selectors', () => { - describe('getAnomalyScoreSeries', () => { - it('should return anomalyScoreSeries', () => { - const data = [{ x0: 0, x: 10 }]; - expect(getAnomalyScoreSeries(data)).toEqual({ - color: '#e7664c', - data: [{ x0: 0, x: 10 }], - title: 'Anomaly score', - type: 'rectAnnotation', - }); - }); - }); - - describe('getResponseTimeSeries', () => { - const apmTimeseries = { - responseTimes: { - avg: [ - { x: 0, y: 100 }, - { x: 1000, y: 200 }, - ], - p95: [ - { x: 0, y: 200 }, - { x: 1000, y: 300 }, - ], - p99: [ - { x: 0, y: 300 }, - { x: 1000, y: 400 }, - ], - }, - tpmBuckets: [], - overallAvgDuration: 200, - }; - - it('should produce correct series', () => { - expect(getResponseTimeSeries({ apmTimeseries })).toEqual([ - { - color: '#6092c0', - data: [ - { x: 0, y: 100 }, - { x: 1000, y: 200 }, - ], - legendValue: '200 μs', - title: 'Avg.', - type: 'linemark', - }, - { - color: '#d6bf57', - data: [ - { x: 0, y: 200 }, - { x: 1000, y: 300 }, - ], - title: '95th percentile', - titleShort: '95th', - type: 'linemark', - }, - { - color: '#da8b45', - data: [ - { x: 0, y: 300 }, - { x: 1000, y: 400 }, - ], - title: '99th percentile', - titleShort: '99th', - type: 'linemark', - }, - ]); - }); - - it('should return 3 series', () => { - expect(getResponseTimeSeries({ apmTimeseries }).length).toBe(3); - }); - }); - - describe('getTpmSeries', () => { - const apmTimeseries: ApmTimeSeriesResponse = { - responseTimes: { - avg: [], - p95: [], - p99: [], - }, - tpmBuckets: [ - { - key: 'HTTP 2xx', - avg: 3.5, - dataPoints: [ - { x: 0, y: 5 }, - { x: 1, y: 2 }, - ], - }, - { key: 'HTTP 4xx', avg: 1, dataPoints: [{ x: 0, y: 1 }] }, - { key: 'HTTP 5xx', avg: 0, dataPoints: [{ x: 0, y: 0 }] }, - ], - overallAvgDuration: 200, - }; - const transactionType = 'MyTransactionType'; - - it('produces correct series', () => { - expect(getTpmSeries(apmTimeseries, transactionType)).toEqual([ - { - color: successColor, - data: [ - { x: 0, y: 5 }, - { x: 1, y: 2 }, - ], - legendValue: '3.5 tpm', - title: 'HTTP 2xx', - type: 'linemark', - }, - { - color: warningColor, - data: [{ x: 0, y: 1 }], - legendValue: '1.0 tpm', - title: 'HTTP 4xx', - type: 'linemark', - }, - { - color: errorColor, - data: [{ x: 0, y: 0 }], - legendValue: '0 tpm', - title: 'HTTP 5xx', - type: 'linemark', - }, - ]); - }); - - describe('with success buckets', () => { - it('uses a success color', () => { - const key = 'it was a success'; - expect( - getTpmSeries({ - ...apmTimeseries, - tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], - })[0].color - ).toEqual(theme.euiColorSecondary); - }); - }); - - describe('with SUCESS buckets', () => { - it('uses a success color', () => { - const key = 'it was a Success'; - expect( - getTpmSeries({ - ...apmTimeseries, - tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], - })[0].color - ).toEqual(theme.euiColorSecondary); - }); - }); - - describe('with ok buckets', () => { - it('uses a success color', () => { - const key = 'it was ok'; - expect( - getTpmSeries({ - ...apmTimeseries, - tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], - })[0].color - ).toEqual(theme.euiColorSecondary); - }); - }); - - describe('with OK buckets', () => { - it('uses a success color', () => { - const key = 'it was OK'; - expect( - getTpmSeries({ - ...apmTimeseries, - tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], - })[0].color - ).toEqual(theme.euiColorSecondary); - }); - }); - - describe('with fail buckets', () => { - it('uses a failure color', () => { - const key = 'it failed'; - expect( - getTpmSeries({ - ...apmTimeseries, - tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], - })[0].color - ).toEqual(theme.euiColorDanger); - }); - }); - - describe('with FAIL buckets', () => { - it('uses a failure color', () => { - const key = 'it FAILED'; - expect( - getTpmSeries({ - ...apmTimeseries, - tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], - })[0].color - ).toEqual(theme.euiColorDanger); - }); - }); - - describe('with error buckets', () => { - it('uses a failure color', () => { - const key = 'Quizás fuera un error'; - expect( - getTpmSeries({ - ...apmTimeseries, - tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], - })[0].color - ).toEqual(theme.euiColorDanger); - }); - }); - - describe('with ERROR buckets', () => { - it('uses a failure color', () => { - const key = 'Quizás fuera un ErroR'; - expect( - getTpmSeries({ - ...apmTimeseries, - tpmBuckets: [{ key, avg: 0, dataPoints: [{ x: 0, y: 0 }] }], - })[0].color - ).toEqual(theme.euiColorDanger); - }); - }); - - describe('when empty', () => { - it('produces an empty series', () => { - const responseTimes = { - avg: [ - { x: 0, y: 1 }, - { x: 100, y: 1 }, - ], - p95: [ - { x: 0, y: 1 }, - { x: 100, y: 1 }, - ], - p99: [ - { x: 0, y: 1 }, - { x: 100, y: 1 }, - ], - }; - const series = getTpmSeries( - { ...apmTimeseries, responseTimes, tpmBuckets: [] }, - transactionType - ); - - expect(series[0].data.length).toBe(11); - expect(series[0].data[0].x).toBe(0); - expect(series[0].data[10].x).toBe(100); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/selectors/chart_selectors.ts b/x-pack/plugins/apm/public/selectors/chart_selectors.ts deleted file mode 100644 index 37bd04e5d9980..0000000000000 --- a/x-pack/plugins/apm/public/selectors/chart_selectors.ts +++ /dev/null @@ -1,213 +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 theme from '@elastic/eui/dist/eui_theme_light.json'; -import { i18n } from '@kbn/i18n'; -import { difference, zipObject } from 'lodash'; -import { rgba } from 'polished'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TimeSeriesAPIResponse } from '../../server/lib/transactions/charts'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ApmTimeSeriesResponse } from '../../server/lib/transactions/charts/get_timeseries_data/transform'; -import { - Coordinate, - RectCoordinate, - TimeSeries, -} from '../../typings/timeseries'; -import { IUrlParams } from '../context/url_params_context/types'; -import { getEmptySeries } from '../components/shared/charts/helper/get_empty_series'; -import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; -import { asDuration, asTransactionRate } from '../../common/utils/formatters'; - -export interface ITpmBucket { - title: string; - data: Coordinate[]; - legendValue: string; - type: string; - color: string; -} - -export interface AnomalySeries { - scores: TimeSeries; - bounderies: TimeSeries; -} - -export interface ITransactionChartData { - tpmSeries?: ITpmBucket[]; - responseTimeSeries?: TimeSeries[]; - mlJobId: string | undefined; - anomalySeries?: AnomalySeries; -} - -const INITIAL_DATA: Partial = { - apmTimeseries: undefined, - anomalyTimeseries: undefined, -}; - -export function getTransactionCharts( - { transactionType }: IUrlParams, - charts = INITIAL_DATA -): ITransactionChartData { - const { apmTimeseries, anomalyTimeseries } = charts; - - const transactionCharts: ITransactionChartData = { - tpmSeries: undefined, - responseTimeSeries: undefined, - mlJobId: anomalyTimeseries?.jobId, - }; - - if (apmTimeseries) { - transactionCharts.tpmSeries = getTpmSeries(apmTimeseries, transactionType); - - transactionCharts.responseTimeSeries = getResponseTimeSeries({ - apmTimeseries, - }); - - transactionCharts.anomalySeries = getResponseTimeAnnomalySeries({ - anomalyTimeseries, - }); - } - return transactionCharts; -} - -function getResponseTimeAnnomalySeries({ - anomalyTimeseries, -}: { - anomalyTimeseries: TimeSeriesAPIResponse['anomalyTimeseries']; -}): AnomalySeries | undefined { - if (anomalyTimeseries) { - return { - bounderies: getAnomalyBoundariesSeries( - anomalyTimeseries.anomalyBoundaries - ), - scores: getAnomalyScoreSeries(anomalyTimeseries.anomalyScore), - }; - } -} - -export function getResponseTimeSeries({ - apmTimeseries, -}: { - apmTimeseries: TimeSeriesAPIResponse['apmTimeseries']; -}) { - const { overallAvgDuration } = apmTimeseries; - const { avg, p95, p99 } = apmTimeseries.responseTimes; - - const series: TimeSeries[] = [ - { - title: i18n.translate('xpack.apm.transactions.chart.averageLabel', { - defaultMessage: 'Avg.', - }), - data: avg, - legendValue: asDuration(overallAvgDuration), - type: 'linemark', - color: theme.euiColorVis1, - }, - { - title: i18n.translate( - 'xpack.apm.transactions.chart.95thPercentileLabel', - { - defaultMessage: '95th percentile', - } - ), - titleShort: '95th', - data: p95, - type: 'linemark', - color: theme.euiColorVis5, - }, - { - title: i18n.translate( - 'xpack.apm.transactions.chart.99thPercentileLabel', - { - defaultMessage: '99th percentile', - } - ), - titleShort: '99th', - data: p99, - type: 'linemark', - color: theme.euiColorVis7, - }, - ]; - - return series; -} - -export function getAnomalyScoreSeries(data: RectCoordinate[]) { - return { - title: i18n.translate('xpack.apm.transactions.chart.anomalyScoreLabel', { - defaultMessage: 'Anomaly score', - }), - data, - type: 'rectAnnotation', - color: theme.euiColorVis9, - }; -} - -function getAnomalyBoundariesSeries(data: Coordinate[]) { - return { - title: i18n.translate( - 'xpack.apm.transactions.chart.anomalyBoundariesLabel', - { - defaultMessage: 'Anomaly Boundaries', - } - ), - data, - type: 'area', - color: rgba(theme.euiColorVis1, 0.5), - }; -} - -export function getTpmSeries( - apmTimeseries: ApmTimeSeriesResponse, - transactionType?: string -) { - const { tpmBuckets } = apmTimeseries; - const bucketKeys = tpmBuckets.map(({ key }) => key); - const getColor = getColorByKey(bucketKeys); - - const { avg } = apmTimeseries.responseTimes; - - if (!tpmBuckets.length && avg.length) { - const start = avg[0].x; - const end = avg[avg.length - 1].x; - return getEmptySeries(start, end); - } - - return tpmBuckets.map((bucket) => { - return { - title: bucket.key, - data: bucket.dataPoints, - legendValue: asTransactionRate(bucket.avg), - type: 'linemark', - color: getColor(bucket.key), - }; - }); -} - -function colorMatch(key: string) { - if (/ok|success/i.test(key)) { - return theme.euiColorSecondary; - } else if (/error|fail/i.test(key)) { - return theme.euiColorDanger; - } -} - -function getColorByKey(keys: string[]) { - const assignedColors = ['HTTP 2xx', 'HTTP 3xx', 'HTTP 4xx', 'HTTP 5xx']; - - const unknownKeys = difference(keys, assignedColors); - const unassignedColors: Record = zipObject(unknownKeys, [ - theme.euiColorVis1, - theme.euiColorVis3, - theme.euiColorVis4, - theme.euiColorVis6, - theme.euiColorVis2, - theme.euiColorVis8, - ]); - - return (key: string) => - colorMatch(key) || httpStatusCodeToColor(key) || unassignedColors[key]; -} diff --git a/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts b/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts new file mode 100644 index 0000000000000..4684742bf4d8b --- /dev/null +++ b/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { EuiTheme } from '../../../xpack_legacy/common'; +import { + getLatencyChartSelector, + LatencyChartsResponse, +} from './latency_chart_selectors'; + +const theme = { + eui: { + euiColorVis1: 'blue', + euiColorVis5: 'red', + euiColorVis7: 'black', + euiColorVis9: 'yellow', + }, +} as EuiTheme; + +const latencyChartData = { + overallAvgDuration: 1, + latencyTimeseries: { + avg: [{ x: 1, y: 10 }], + p95: [{ x: 2, y: 5 }], + p99: [{ x: 3, y: 8 }], + }, + anomalyTimeseries: { + jobId: '1', + anomalyBoundaries: [{ x: 1, y: 2 }], + anomalyScore: [{ x: 1, x0: 2 }], + }, +} as LatencyChartsResponse; + +describe('getLatencyChartSelector', () => { + describe('without anomaly', () => { + it('returns default values when data is undefined', () => { + const latencyChart = getLatencyChartSelector({ theme }); + expect(latencyChart).toEqual({ + latencyTimeseries: [], + mlJobId: undefined, + anomalyTimeseries: undefined, + }); + }); + it('returns latency time series', () => { + const { anomalyTimeseries, ...latencyWithouAnomaly } = latencyChartData; + const latencyTimeseries = getLatencyChartSelector({ + latencyChart: latencyWithouAnomaly as LatencyChartsResponse, + theme, + }); + expect(latencyTimeseries).toEqual({ + latencyTimeseries: [ + { + title: 'Avg.', + data: [{ x: 1, y: 10 }], + legendValue: '1 μs', + type: 'linemark', + color: 'blue', + }, + { + title: '95th percentile', + titleShort: '95th', + data: [{ x: 2, y: 5 }], + type: 'linemark', + color: 'red', + }, + { + title: '99th percentile', + titleShort: '99th', + data: [{ x: 3, y: 8 }], + type: 'linemark', + color: 'black', + }, + ], + }); + }); + }); + + describe('with anomaly', () => { + it('returns latency time series and anomaly timeseries', () => { + const latencyTimeseries = getLatencyChartSelector({ + latencyChart: latencyChartData, + theme, + }); + expect(latencyTimeseries).toEqual({ + latencyTimeseries: [ + { + title: 'Avg.', + data: [{ x: 1, y: 10 }], + legendValue: '1 μs', + type: 'linemark', + color: 'blue', + }, + { + title: '95th percentile', + titleShort: '95th', + data: [{ x: 2, y: 5 }], + type: 'linemark', + color: 'red', + }, + { + title: '99th percentile', + titleShort: '99th', + data: [{ x: 3, y: 8 }], + type: 'linemark', + color: 'black', + }, + ], + mlJobId: '1', + anomalyTimeseries: { + bounderies: { + title: 'Anomaly Boundaries', + data: [{ x: 1, y: 2 }], + type: 'area', + color: 'rgba(0,0,255,0.5)', + }, + scores: { + title: 'Anomaly score', + data: [{ x: 1, x0: 2 }], + type: 'rectAnnotation', + color: 'yellow', + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts new file mode 100644 index 0000000000000..73b855e12d96e --- /dev/null +++ b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts @@ -0,0 +1,145 @@ +/* + * 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 { rgba } from 'polished'; +import { EuiTheme } from '../../../observability/public'; +import { asDuration } from '../../common/utils/formatters'; +import { + Coordinate, + RectCoordinate, + TimeSeries, +} from '../../typings/timeseries'; +import { APIReturnType } from '../services/rest/createCallApmApi'; + +export type LatencyChartsResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/latency'>; + +interface LatencyChart { + latencyTimeseries: TimeSeries[]; + mlJobId?: string; + anomalyTimeseries?: { + bounderies: TimeSeries; + scores: TimeSeries; + }; +} + +export function getLatencyChartSelector({ + latencyChart, + theme, +}: { + latencyChart?: LatencyChartsResponse; + theme: EuiTheme; +}): LatencyChart { + if (!latencyChart) { + return { + latencyTimeseries: [], + mlJobId: undefined, + anomalyTimeseries: undefined, + }; + } + return { + latencyTimeseries: getLatencyTimeseries({ latencyChart, theme }), + mlJobId: latencyChart.anomalyTimeseries?.jobId, + anomalyTimeseries: getAnnomalyTimeseries({ + anomalyTimeseries: latencyChart.anomalyTimeseries, + theme, + }), + }; +} + +function getLatencyTimeseries({ + latencyChart, + theme, +}: { + latencyChart: LatencyChartsResponse; + theme: EuiTheme; +}) { + const { overallAvgDuration } = latencyChart; + const { avg, p95, p99 } = latencyChart.latencyTimeseries; + + const series = [ + { + title: i18n.translate( + 'xpack.apm.transactions.latency.chart.averageLabel', + { + defaultMessage: 'Avg.', + } + ), + data: avg, + legendValue: asDuration(overallAvgDuration), + type: 'linemark', + color: theme.eui.euiColorVis1, + }, + { + title: i18n.translate( + 'xpack.apm.transactions.latency.chart.95thPercentileLabel', + { + defaultMessage: '95th percentile', + } + ), + titleShort: '95th', + data: p95, + type: 'linemark', + color: theme.eui.euiColorVis5, + }, + { + title: i18n.translate( + 'xpack.apm.transactions.latency.chart.99thPercentileLabel', + { + defaultMessage: '99th percentile', + } + ), + titleShort: '99th', + data: p99, + type: 'linemark', + color: theme.eui.euiColorVis7, + }, + ]; + + return series; +} + +function getAnnomalyTimeseries({ + anomalyTimeseries, + theme, +}: { + anomalyTimeseries: LatencyChartsResponse['anomalyTimeseries']; + theme: EuiTheme; +}) { + if (anomalyTimeseries) { + return { + bounderies: getAnomalyBoundariesSeries( + anomalyTimeseries.anomalyBoundaries, + theme + ), + scores: getAnomalyScoreSeries(anomalyTimeseries.anomalyScore, theme), + }; + } +} + +export function getAnomalyScoreSeries(data: RectCoordinate[], theme: EuiTheme) { + return { + title: i18n.translate('xpack.apm.transactions.chart.anomalyScoreLabel', { + defaultMessage: 'Anomaly score', + }), + data, + type: 'rectAnnotation', + color: theme.eui.euiColorVis9, + }; +} + +function getAnomalyBoundariesSeries(data: Coordinate[], theme: EuiTheme) { + return { + title: i18n.translate( + 'xpack.apm.transactions.chart.anomalyBoundariesLabel', + { + defaultMessage: 'Anomaly Boundaries', + } + ), + data, + type: 'area', + color: rgba(theme.eui.euiColorVis1, 0.5), + }; +} diff --git a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts new file mode 100644 index 0000000000000..ac85142f3050b --- /dev/null +++ b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiTheme } from '../../../observability/public'; +import { + getThrouputChartSelector, + ThrouputChartsResponse, +} from './throuput_chart_selectors'; + +const theme = { + eui: { + euiColorVis1: 'green', + euiColorVis2: 'black', + euiColorVis3: 'gray', + euiColorVis4: 'blue', + euiColorVis6: 'red', + euiColorVis8: 'yellow', + euiColorSecondary: 'white', + euiColorDanger: 'purple', + }, +} as EuiTheme; + +const throughputData = { + throughputTimeseries: [ + { key: 'HTTP 2xx', avg: 1, dataPoints: [{ x: 1, y: 2 }] }, + { key: 'HTTP 4xx', avg: 1, dataPoints: [{ x: 1, y: 2 }] }, + { key: 'HTTP 5xx', avg: 1, dataPoints: [{ x: 1, y: 2 }] }, + ], +} as ThrouputChartsResponse; + +describe('getThrouputChartSelector', () => { + it('returns default values when data is undefined', () => { + const throughputTimeseries = getThrouputChartSelector({ theme }); + expect(throughputTimeseries).toEqual({ throughputTimeseries: [] }); + }); + + it('return throughput time series', () => { + const throughputTimeseries = getThrouputChartSelector({ + theme, + throuputChart: throughputData, + }); + + expect(throughputTimeseries).toEqual({ + throughputTimeseries: [ + { + title: 'HTTP 2xx', + data: [{ x: 1, y: 2 }], + legendValue: '1.0 tpm', + type: 'linemark', + color: '#327a42', + }, + { + title: 'HTTP 4xx', + data: [{ x: 1, y: 2 }], + legendValue: '1.0 tpm', + type: 'linemark', + color: '#f5a700', + }, + { + title: 'HTTP 5xx', + data: [{ x: 1, y: 2 }], + legendValue: '1.0 tpm', + type: 'linemark', + color: '#c23c2b', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts new file mode 100644 index 0000000000000..701558b154677 --- /dev/null +++ b/x-pack/plugins/apm/public/selectors/throuput_chart_selectors.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { difference, zipObject } from 'lodash'; +import { EuiTheme } from '../../../observability/public'; +import { asTransactionRate } from '../../common/utils/formatters'; +import { TimeSeries } from '../../typings/timeseries'; +import { getEmptySeries } from '../components/shared/charts/helper/get_empty_series'; +import { APIReturnType } from '../services/rest/createCallApmApi'; +import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; + +export type ThrouputChartsResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/throughput'>; + +interface ThroughputChart { + throughputTimeseries: TimeSeries[]; +} + +export function getThrouputChartSelector({ + theme, + throuputChart, +}: { + theme: EuiTheme; + throuputChart?: ThrouputChartsResponse; +}): ThroughputChart { + if (!throuputChart) { + return { throughputTimeseries: [] }; + } + + return { + throughputTimeseries: getThroughputTimeseries({ throuputChart, theme }), + }; +} + +export function getThroughputTimeseries({ + throuputChart, + theme, +}: { + theme: EuiTheme; + throuputChart: ThrouputChartsResponse; +}) { + const { throughputTimeseries } = throuputChart; + const bucketKeys = throughputTimeseries.map(({ key }) => key); + const getColor = getColorByKey(bucketKeys, theme); + + if (!throughputTimeseries.length) { + const start = throughputTimeseries[0].dataPoints[0].x; + const end = + throughputTimeseries[0].dataPoints[ + throughputTimeseries[0].dataPoints.length - 1 + ].x; + return getEmptySeries(start, end); + } + + return throughputTimeseries.map((bucket) => { + return { + title: bucket.key, + data: bucket.dataPoints, + legendValue: asTransactionRate(bucket.avg), + type: 'linemark', + color: getColor(bucket.key), + }; + }); +} + +function colorMatch(key: string, theme: EuiTheme) { + if (/ok|success/i.test(key)) { + return theme.eui.euiColorSecondary; + } else if (/error|fail/i.test(key)) { + return theme.eui.euiColorDanger; + } +} + +function getColorByKey(keys: string[], theme: EuiTheme) { + const assignedColors = ['HTTP 2xx', 'HTTP 3xx', 'HTTP 4xx', 'HTTP 5xx']; + + const unknownKeys = difference(keys, assignedColors); + const unassignedColors: Record = zipObject(unknownKeys, [ + theme.eui.euiColorVis1, + theme.eui.euiColorVis3, + theme.eui.euiColorVis4, + theme.eui.euiColorVis6, + theme.eui.euiColorVis2, + theme.eui.euiColorVis8, + ]); + + return (key: string) => + colorMatch(key, theme) || + httpStatusCodeToColor(key) || + unassignedColors[key]; +} diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap index 085bedf774c46..43fe4dfe752e6 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/__snapshots__/get_buckets.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`timeseriesFetcher should make the correct query 1`] = ` +exports[`get buckets should make the correct query 1`] = ` Array [ Array [ Object { diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index 50da1f9c20d16..ff7d05efc1802 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -8,7 +8,7 @@ import { getBuckets } from '../get_buckets'; import { APMConfig } from '../../../..'; import { ProcessorEvent } from '../../../../../common/processor_event'; -describe('timeseriesFetcher', () => { +describe('get buckets', () => { let clientSpy: jest.Mock; beforeEach(async () => { diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 623abf6930297..3903298415aed 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -5,6 +5,7 @@ */ import { LegacyAPICaller, Logger } from 'kibana/server'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; @@ -28,34 +29,31 @@ export async function getStoredAnnotations({ annotationsClient: ScopedAnnotationsClient; logger: Logger; }): Promise { - try { - const response: ESSearchResponse = (await apiCaller( - 'search', - { - index: annotationsClient.index, - body: { - size: 50, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: setup.start, - lt: setup.end, - }, - }, - }, - { term: { 'annotation.type': 'deployment' } }, - { term: { tags: 'apm' } }, - { term: { [SERVICE_NAME]: serviceName } }, - ...getEnvironmentUiFilterES(environment), - ], - }, + const body = { + size: 50, + query: { + bool: { + filter: [ + { + range: rangeFilter(setup.start, setup.end), }, - }, - } - )) as any; + { term: { 'annotation.type': 'deployment' } }, + { term: { tags: 'apm' } }, + { term: { [SERVICE_NAME]: serviceName } }, + ...getEnvironmentUiFilterES(environment), + ], + }, + }, + }; + + try { + const response: ESSearchResponse< + ESAnnotation, + { body: typeof body } + > = (await apiCaller('search', { + index: annotationsClient.index, + body, + })) as any; return response.hits.hits.map((hit) => { return { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts new file mode 100644 index 0000000000000..d6198e2d3b65a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.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 { isEqual, keyBy, mapValues } from 'lodash'; +import { pickKeys } from '../../../../common/utils/pick_keys'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { + AGENT_NAME, + EVENT_OUTCOME, + PARENT_ID, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_ID, + SPAN_SUBTYPE, + SPAN_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { joinByKey } from '../../../../common/utils/join_by_key'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export const getDestinationMap = async ({ + setup, + serviceName, + environment, +}: { + setup: Setup & SetupTimeRange; + serviceName: string; + environment: string; +}) => { + const { start, end, apmEventClient } = setup; + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.span], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } }, + { range: rangeFilter(start, end) }, + ...getEnvironmentUiFilterES(environment), + ], + }, + }, + aggs: { + connections: { + composite: { + size: 1000, + sources: [ + { + [SPAN_DESTINATION_SERVICE_RESOURCE]: { + terms: { field: SPAN_DESTINATION_SERVICE_RESOURCE }, + }, + }, + // make sure we get samples for both successful + // and failed calls + { [EVENT_OUTCOME]: { terms: { field: EVENT_OUTCOME } } }, + ], + }, + aggs: { + docs: { + top_hits: { + docvalue_fields: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID] as const, + _source: false, + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + const outgoingConnections = + response.aggregations?.connections.buckets.map((bucket) => { + const doc = bucket.docs.hits.hits[0]; + + return { + [SPAN_DESTINATION_SERVICE_RESOURCE]: String( + bucket.key[SPAN_DESTINATION_SERVICE_RESOURCE] + ), + [SPAN_ID]: String(doc.fields[SPAN_ID]?.[0]), + [SPAN_TYPE]: String(doc.fields[SPAN_TYPE]?.[0] ?? ''), + [SPAN_SUBTYPE]: String(doc.fields[SPAN_SUBTYPE]?.[0] ?? ''), + }; + }) ?? []; + + const transactionResponse = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + { + terms: { + [PARENT_ID]: outgoingConnections.map( + (connection) => connection[SPAN_ID] + ), + }, + }, + { range: rangeFilter(start, end) }, + ], + }, + }, + size: outgoingConnections.length, + docvalue_fields: [ + SERVICE_NAME, + SERVICE_ENVIRONMENT, + AGENT_NAME, + PARENT_ID, + ] as const, + _source: false, + }, + }); + + const incomingConnections = transactionResponse.hits.hits.map((hit) => ({ + [SPAN_ID]: String(hit.fields[PARENT_ID]![0]), + service: { + name: String(hit.fields[SERVICE_NAME]![0]), + environment: String(hit.fields[SERVICE_ENVIRONMENT]?.[0] ?? ''), + agentName: hit.fields[AGENT_NAME]![0] as AgentName, + }, + })); + + // merge outgoing spans with transactions by span.id/parent.id + const joinedBySpanId = joinByKey( + [...outgoingConnections, ...incomingConnections], + SPAN_ID + ); + + // we could have multiple connections per address because + // of multiple event outcomes + const dedupedConnectionsByAddress = joinByKey( + joinedBySpanId, + SPAN_DESTINATION_SERVICE_RESOURCE + ); + + // identify a connection by either service.name, service.environment, agent.name + // OR span.destination.service.resource + + const connectionsWithId = dedupedConnectionsByAddress.map((connection) => { + const id = + 'service' in connection + ? { service: connection.service } + : pickKeys(connection, SPAN_DESTINATION_SERVICE_RESOURCE); + + return { + ...connection, + id, + }; + }); + + const dedupedConnectionsById = joinByKey(connectionsWithId, 'id'); + + const connectionsByAddress = keyBy( + connectionsWithId, + SPAN_DESTINATION_SERVICE_RESOURCE + ); + + // per span.destination.service.resource, return merged/deduped item + return mapValues(connectionsByAddress, ({ id }) => { + const connection = dedupedConnectionsById.find((dedupedConnection) => + isEqual(id, dedupedConnection.id) + )!; + + return { + id, + span: { + type: connection[SPAN_TYPE], + subtype: connection[SPAN_SUBTYPE], + destination: { + service: { + resource: connection[SPAN_DESTINATION_SERVICE_RESOURCE], + }, + }, + }, + ...('service' in connection && connection.service + ? { + service: { + name: connection.service.name, + environment: connection.service.environment, + }, + agent: { + name: connection.service.agentName, + }, + } + : {}), + }; + }); +}; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts new file mode 100644 index 0000000000000..40b8d3e7054c5 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sum } from 'lodash'; +import { + EVENT_OUTCOME, + SERVICE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, +} from '../../../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export const getMetrics = async ({ + setup, + serviceName, + environment, + numBuckets, +}: { + setup: Setup & SetupTimeRange; + serviceName: string; + environment: string; + numBuckets: number; +}) => { + const { start, end, apmEventClient } = setup; + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, + body: { + track_total_hits: true, + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { exists: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT } }, + { range: rangeFilter(start, end) }, + ...getEnvironmentUiFilterES(environment), + ], + }, + }, + aggs: { + connections: { + terms: { + field: SPAN_DESTINATION_SERVICE_RESOURCE, + size: 100, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize({ start, end, numBuckets }) + .intervalString, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: { + latency_sum: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, + }, + }, + count: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, + }, + [EVENT_OUTCOME]: { + terms: { + field: EVENT_OUTCOME, + }, + aggs: { + count: { + sum: { + field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + return ( + response.aggregations?.connections.buckets.map((bucket) => ({ + span: { + destination: { + service: { + resource: String(bucket.key), + }, + }, + }, + value: { + count: sum( + bucket.timeseries.buckets.map( + (dateBucket) => dateBucket.count.value ?? 0 + ) + ), + latency_sum: sum( + bucket.timeseries.buckets.map( + (dateBucket) => dateBucket.latency_sum.value ?? 0 + ) + ), + error_count: sum( + bucket.timeseries.buckets.flatMap( + (dateBucket) => + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0 + ) + ), + }, + timeseries: bucket.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + count: dateBucket.count.value ?? 0, + latency_sum: dateBucket.latency_sum.value ?? 0, + error_count: + dateBucket[EVENT_OUTCOME].buckets.find( + (outcomeBucket) => outcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0, + })), + })) ?? [] + ); +}; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts new file mode 100644 index 0000000000000..0ac881aeac00e --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts @@ -0,0 +1,209 @@ +/* + * 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 { ValuesType } from 'utility-types'; +import { merge } from 'lodash'; +import { SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../../common/elasticsearch_fieldnames'; +import { maybe } from '../../../../common/utils/maybe'; +import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { joinByKey } from '../../../../common/utils/join_by_key'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getMetrics } from './get_metrics'; +import { getDestinationMap } from './get_destination_map'; + +export type ServiceDependencyItem = { + name: string; + latency: { + value: number | null; + timeseries: Array<{ x: number; y: number | null }>; + }; + throughput: { + value: number | null; + timeseries: Array<{ x: number; y: number | null }>; + }; + errorRate: { + value: number | null; + timeseries: Array<{ x: number; y: number | null }>; + }; + impact: number; +} & ( + | { + type: 'service'; + serviceName: string; + agentName: AgentName; + environment?: string; + } + | { type: 'external'; spanType?: string; spanSubtype?: string } +); + +export async function getServiceDependencies({ + setup, + serviceName, + environment, + numBuckets, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + environment: string; + numBuckets: number; +}): Promise { + const { start, end } = setup; + + const [allMetrics, destinationMap] = await Promise.all([ + getMetrics({ + setup, + serviceName, + environment, + numBuckets, + }), + getDestinationMap({ + setup, + serviceName, + environment, + }), + ]); + + const metricsWithDestinationIds = allMetrics.map((metricItem) => { + const spanDestination = metricItem.span.destination.service.resource; + + const destination = maybe(destinationMap[spanDestination]); + const id = destination?.id || { + [SPAN_DESTINATION_SERVICE_RESOURCE]: spanDestination, + }; + + return merge( + { + id, + metrics: [metricItem], + span: { + destination: { + service: { + resource: spanDestination, + }, + }, + }, + }, + destination + ); + }, []); + + const metricsJoinedByDestinationId = joinByKey( + metricsWithDestinationIds, + 'id', + (a, b) => { + const { metrics: metricsA, ...itemA } = a; + const { metrics: metricsB, ...itemB } = b; + + return merge({}, itemA, itemB, { metrics: metricsA.concat(metricsB) }); + } + ); + + const metricsByResolvedAddress = metricsJoinedByDestinationId.map((item) => { + const mergedMetrics = item.metrics.reduce< + Omit, 'span'> + >( + (prev, current) => { + return { + value: { + count: prev.value.count + current.value.count, + latency_sum: prev.value.latency_sum + current.value.latency_sum, + error_count: prev.value.error_count + current.value.error_count, + }, + timeseries: joinByKey( + [...prev.timeseries, ...current.timeseries], + 'x', + (a, b) => ({ + x: a.x, + count: a.count + b.count, + latency_sum: a.latency_sum + b.latency_sum, + error_count: a.error_count + b.error_count, + }) + ), + }; + }, + { + value: { + count: 0, + latency_sum: 0, + error_count: 0, + }, + timeseries: [], + } + ); + + const deltaAsMinutes = (end - start) / 60 / 1000; + + const destMetrics = { + latency: { + value: + mergedMetrics.value.count > 0 + ? mergedMetrics.value.latency_sum / mergedMetrics.value.count + : null, + timeseries: mergedMetrics.timeseries.map((point) => ({ + x: point.x, + y: point.count > 0 ? point.latency_sum / point.count : null, + })), + }, + throughput: { + value: + mergedMetrics.value.count > 0 + ? mergedMetrics.value.count / deltaAsMinutes + : null, + timeseries: mergedMetrics.timeseries.map((point) => ({ + x: point.x, + y: point.count > 0 ? point.count / deltaAsMinutes : null, + })), + }, + errorRate: { + value: + mergedMetrics.value.count > 0 + ? (mergedMetrics.value.error_count ?? 0) / mergedMetrics.value.count + : null, + timeseries: mergedMetrics.timeseries.map((point) => ({ + x: point.x, + y: point.count > 0 ? (point.error_count ?? 0) / point.count : null, + })), + }, + }; + + if (item.service) { + return { + name: item.service.name, + type: 'service' as const, + serviceName: item.service.name, + environment: item.service.environment, + // agent.name should always be there, type returned from joinByKey is too pessimistic + agentName: item.agent!.name, + ...destMetrics, + }; + } + + return { + name: item.span.destination.service.resource, + type: 'external' as const, + spanType: item.span.type, + spanSubtype: item.span.subtype, + ...destMetrics, + }; + }); + + const latencySums = metricsByResolvedAddress + .map((metrics) => metrics.latency.value) + .filter(isFiniteNumber); + + const minLatencySum = Math.min(...latencySums); + const maxLatencySum = Math.max(...latencySums); + + return metricsByResolvedAddress.map((metric) => ({ + ...metric, + impact: + metric.latency.value === null + ? 0 + : ((metric.latency.value - minLatencySum) / + (maxLatencySum - minLatencySum)) * + 100, + })); +} diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index 3e0a7317afd70..5d6a92a874111 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -311,324 +311,6 @@ Object { } `; -exports[`transaction queries fetches transaction charts 1`] = ` -Object { - "apm": Object { - "events": Array [ - "transaction", - ], - }, - "body": Object { - "aggs": Object { - "overall_avg_duration": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "response_times": Object { - "aggs": Object { - "avg": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "pct": Object { - "percentiles": Object { - "field": "transaction.duration.us", - "hdr": Object { - "number_of_significant_value_digits": 2, - }, - "percents": Array [ - 95, - 99, - ], - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "10800s", - "min_doc_count": 0, - }, - }, - "transaction_results": Object { - "aggs": Object { - "timeseries": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "10800s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "transaction.result", - "missing": "", - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "foo", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - ], - }, - }, - "size": 0, - }, -} -`; - -exports[`transaction queries fetches transaction charts for a transaction type 1`] = ` -Object { - "apm": Object { - "events": Array [ - "transaction", - ], - }, - "body": Object { - "aggs": Object { - "overall_avg_duration": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "response_times": Object { - "aggs": Object { - "avg": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "pct": Object { - "percentiles": Object { - "field": "transaction.duration.us", - "hdr": Object { - "number_of_significant_value_digits": 2, - }, - "percents": Array [ - 95, - 99, - ], - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "10800s", - "min_doc_count": 0, - }, - }, - "transaction_results": Object { - "aggs": Object { - "timeseries": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "10800s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "transaction.result", - "missing": "", - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "foo", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - Object { - "term": Object { - "transaction.name": "bar", - }, - }, - ], - }, - }, - "size": 0, - }, -} -`; - -exports[`transaction queries fetches transaction charts for a transaction type and transaction name 1`] = ` -Object { - "apm": Object { - "events": Array [ - "transaction", - ], - }, - "body": Object { - "aggs": Object { - "overall_avg_duration": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "response_times": Object { - "aggs": Object { - "avg": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "pct": Object { - "percentiles": Object { - "field": "transaction.duration.us", - "hdr": Object { - "number_of_significant_value_digits": 2, - }, - "percents": Array [ - 95, - 99, - ], - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "10800s", - "min_doc_count": 0, - }, - }, - "transaction_results": Object { - "aggs": Object { - "timeseries": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "10800s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "transaction.result", - "missing": "", - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "foo", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - Object { - "term": Object { - "transaction.name": "bar", - }, - }, - Object { - "term": Object { - "transaction.type": "baz", - }, - }, - ], - }, - }, - "size": 0, - }, -} -`; - exports[`transaction queries fetches transaction distribution 1`] = ` Object { "apm": Object { diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap deleted file mode 100644 index fb696b40f4ab4..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap +++ /dev/null @@ -1,111 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`timeseriesFetcher should call client with correct query 1`] = ` -Array [ - Array [ - Object { - "apm": Object { - "events": Array [ - "transaction", - ], - }, - "body": Object { - "aggs": Object { - "overall_avg_duration": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "response_times": Object { - "aggs": Object { - "avg": Object { - "avg": Object { - "field": "transaction.duration.us", - }, - }, - "pct": Object { - "percentiles": Object { - "field": "transaction.duration.us", - "hdr": Object { - "number_of_significant_value_digits": 2, - }, - "percents": Array [ - 95, - 99, - ], - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "10800s", - "min_doc_count": 0, - }, - }, - "transaction_results": Object { - "aggs": Object { - "timeseries": Object { - "aggs": Object { - "count": Object { - "value_count": Object { - "field": "transaction.duration.us", - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 1528977600000, - "min": 1528113600000, - }, - "field": "@timestamp", - "fixed_interval": "10800s", - "min_doc_count": 0, - }, - }, - }, - "terms": Object { - "field": "transaction.result", - "missing": "", - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "myServiceName", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - Object { - "term": Object { - "service.environment": "test", - }, - }, - Object { - "term": Object { - "transaction.type": "myTransactionType", - }, - }, - ], - }, - }, - "size": 0, - }, - }, - ], -] -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap deleted file mode 100644 index 46d6c1425d599..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap +++ /dev/null @@ -1,4106 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`timeseriesTransformer should match snapshot 1`] = ` -Object { - "overallAvgDuration": 73065.05176360115, - "responseTimes": Object { - "avg": Array [ - Object { - "x": 1593852000000, - "y": null, - }, - Object { - "x": 1593852600000, - "y": null, - }, - Object { - "x": 1593853200000, - "y": null, - }, - Object { - "x": 1593853800000, - "y": null, - }, - Object { - "x": 1593854400000, - "y": null, - }, - Object { - "x": 1593855000000, - "y": null, - }, - Object { - "x": 1593855600000, - "y": null, - }, - Object { - "x": 1593856200000, - "y": null, - }, - Object { - "x": 1593856800000, - "y": null, - }, - Object { - "x": 1593857400000, - "y": null, - }, - Object { - "x": 1593858000000, - "y": null, - }, - Object { - "x": 1593858600000, - "y": null, - }, - Object { - "x": 1593859200000, - "y": null, - }, - Object { - "x": 1593859800000, - "y": null, - }, - Object { - "x": 1593860400000, - "y": null, - }, - Object { - "x": 1593861000000, - "y": null, - }, - Object { - "x": 1593861600000, - "y": null, - }, - Object { - "x": 1593862200000, - "y": null, - }, - Object { - "x": 1593862800000, - "y": null, - }, - Object { - "x": 1593863400000, - "y": null, - }, - Object { - "x": 1593864000000, - "y": null, - }, - Object { - "x": 1593864600000, - "y": null, - }, - Object { - "x": 1593865200000, - "y": null, - }, - Object { - "x": 1593865800000, - "y": null, - }, - Object { - "x": 1593866400000, - "y": null, - }, - Object { - "x": 1593867000000, - "y": null, - }, - Object { - "x": 1593867600000, - "y": null, - }, - Object { - "x": 1593868200000, - "y": null, - }, - Object { - "x": 1593868800000, - "y": null, - }, - Object { - "x": 1593869400000, - "y": null, - }, - Object { - "x": 1593870000000, - "y": null, - }, - Object { - "x": 1593870600000, - "y": null, - }, - Object { - "x": 1593871200000, - "y": null, - }, - Object { - "x": 1593871800000, - "y": null, - }, - Object { - "x": 1593872400000, - "y": null, - }, - Object { - "x": 1593873000000, - "y": null, - }, - Object { - "x": 1593873600000, - "y": null, - }, - Object { - "x": 1593874200000, - "y": null, - }, - Object { - "x": 1593874800000, - "y": null, - }, - Object { - "x": 1593875400000, - "y": null, - }, - Object { - "x": 1593876000000, - "y": null, - }, - Object { - "x": 1593876600000, - "y": null, - }, - Object { - "x": 1593877200000, - "y": null, - }, - Object { - "x": 1593877800000, - "y": null, - }, - Object { - "x": 1593878400000, - "y": null, - }, - Object { - "x": 1593879000000, - "y": null, - }, - Object { - "x": 1593879600000, - "y": null, - }, - Object { - "x": 1593880200000, - "y": null, - }, - Object { - "x": 1593880800000, - "y": null, - }, - Object { - "x": 1593881400000, - "y": null, - }, - Object { - "x": 1593882000000, - "y": null, - }, - Object { - "x": 1593882600000, - "y": null, - }, - Object { - "x": 1593883200000, - "y": null, - }, - Object { - "x": 1593883800000, - "y": null, - }, - Object { - "x": 1593884400000, - "y": null, - }, - Object { - "x": 1593885000000, - "y": null, - }, - Object { - "x": 1593885600000, - "y": null, - }, - Object { - "x": 1593886200000, - "y": null, - }, - Object { - "x": 1593886800000, - "y": null, - }, - Object { - "x": 1593887400000, - "y": null, - }, - Object { - "x": 1593888000000, - "y": null, - }, - Object { - "x": 1593888600000, - "y": 43364.46153846154, - }, - Object { - "x": 1593889200000, - "y": 147903.58671586716, - }, - Object { - "x": 1593889800000, - "y": 57370.52342487884, - }, - Object { - "x": 1593890400000, - "y": 59687.82558139535, - }, - Object { - "x": 1593891000000, - "y": 51810.68111455108, - }, - Object { - "x": 1593891600000, - "y": 51736.59420289855, - }, - Object { - "x": 1593892200000, - "y": 37241.293224299065, - }, - Object { - "x": 1593892800000, - "y": 49444.90771558245, - }, - Object { - "x": 1593893400000, - "y": 56807.80495356037, - }, - Object { - "x": 1593894000000, - "y": 43238.74519846351, - }, - Object { - "x": 1593894600000, - "y": 51754.80149253731, - }, - Object { - "x": 1593895200000, - "y": 47166.5964343598, - }, - Object { - "x": 1593895800000, - "y": 41854.688405797104, - }, - Object { - "x": 1593896400000, - "y": 30464.317912218266, - }, - Object { - "x": 1593897000000, - "y": 41558.531380753135, - }, - Object { - "x": 1593897600000, - "y": 41159.68345323741, - }, - Object { - "x": 1593898200000, - "y": 34211.03967168263, - }, - Object { - "x": 1593898800000, - "y": 41322.30621301775, - }, - Object { - "x": 1593899400000, - "y": 42301.523605150214, - }, - Object { - "x": 1593900000000, - "y": 59615.69343065693, - }, - Object { - "x": 1593900600000, - "y": 29567.520050125313, - }, - Object { - "x": 1593901200000, - "y": 56104.7484375, - }, - Object { - "x": 1593901800000, - "y": 40900.70954356847, - }, - Object { - "x": 1593902400000, - "y": null, - }, - Object { - "x": 1593903000000, - "y": null, - }, - Object { - "x": 1593903600000, - "y": 141618.04, - }, - Object { - "x": 1593904200000, - "y": null, - }, - Object { - "x": 1593904800000, - "y": null, - }, - Object { - "x": 1593905400000, - "y": null, - }, - Object { - "x": 1593906000000, - "y": 380742.48780487804, - }, - Object { - "x": 1593906600000, - "y": null, - }, - Object { - "x": 1593907200000, - "y": null, - }, - Object { - "x": 1593907800000, - "y": null, - }, - Object { - "x": 1593908400000, - "y": null, - }, - Object { - "x": 1593909000000, - "y": null, - }, - Object { - "x": 1593909600000, - "y": null, - }, - Object { - "x": 1593910200000, - "y": 122524.7027027027, - }, - Object { - "x": 1593910800000, - "y": null, - }, - Object { - "x": 1593911400000, - "y": null, - }, - Object { - "x": 1593912000000, - "y": null, - }, - Object { - "x": 1593912600000, - "y": null, - }, - Object { - "x": 1593913200000, - "y": null, - }, - Object { - "x": 1593913800000, - "y": null, - }, - Object { - "x": 1593914400000, - "y": null, - }, - Object { - "x": 1593915000000, - "y": null, - }, - Object { - "x": 1593915600000, - "y": null, - }, - Object { - "x": 1593916200000, - "y": null, - }, - Object { - "x": 1593916800000, - "y": 160060.1081081081, - }, - Object { - "x": 1593917400000, - "y": null, - }, - Object { - "x": 1593918000000, - "y": null, - }, - Object { - "x": 1593918600000, - "y": null, - }, - Object { - "x": 1593919200000, - "y": null, - }, - Object { - "x": 1593919800000, - "y": null, - }, - Object { - "x": 1593920400000, - "y": null, - }, - Object { - "x": 1593921000000, - "y": null, - }, - Object { - "x": 1593921600000, - "y": null, - }, - Object { - "x": 1593922200000, - "y": null, - }, - Object { - "x": 1593922800000, - "y": null, - }, - Object { - "x": 1593923400000, - "y": 70357.234375, - }, - Object { - "x": 1593924000000, - "y": null, - }, - Object { - "x": 1593924600000, - "y": null, - }, - Object { - "x": 1593925200000, - "y": null, - }, - Object { - "x": 1593925800000, - "y": null, - }, - Object { - "x": 1593926400000, - "y": null, - }, - Object { - "x": 1593927000000, - "y": null, - }, - Object { - "x": 1593927600000, - "y": null, - }, - Object { - "x": 1593928200000, - "y": null, - }, - Object { - "x": 1593928800000, - "y": null, - }, - Object { - "x": 1593929400000, - "y": null, - }, - Object { - "x": 1593930000000, - "y": 269745.9036144578, - }, - Object { - "x": 1593930600000, - "y": null, - }, - Object { - "x": 1593931200000, - "y": null, - }, - Object { - "x": 1593931800000, - "y": null, - }, - Object { - "x": 1593932400000, - "y": 313349.95238095237, - }, - Object { - "x": 1593933000000, - "y": null, - }, - Object { - "x": 1593933600000, - "y": null, - }, - Object { - "x": 1593934200000, - "y": null, - }, - Object { - "x": 1593934800000, - "y": null, - }, - Object { - "x": 1593935400000, - "y": null, - }, - Object { - "x": 1593936000000, - "y": null, - }, - Object { - "x": 1593936600000, - "y": 397251.288372093, - }, - Object { - "x": 1593937200000, - "y": 361953.5931174089, - }, - Object { - "x": 1593937800000, - "y": 259173.0694980695, - }, - Object { - "x": 1593938400000, - "y": 79648.20935412026, - }, - ], - "p95": Array [ - Object { - "x": 1593852000000, - "y": null, - }, - Object { - "x": 1593852600000, - "y": null, - }, - Object { - "x": 1593853200000, - "y": null, - }, - Object { - "x": 1593853800000, - "y": null, - }, - Object { - "x": 1593854400000, - "y": null, - }, - Object { - "x": 1593855000000, - "y": null, - }, - Object { - "x": 1593855600000, - "y": null, - }, - Object { - "x": 1593856200000, - "y": null, - }, - Object { - "x": 1593856800000, - "y": null, - }, - Object { - "x": 1593857400000, - "y": null, - }, - Object { - "x": 1593858000000, - "y": null, - }, - Object { - "x": 1593858600000, - "y": null, - }, - Object { - "x": 1593859200000, - "y": null, - }, - Object { - "x": 1593859800000, - "y": null, - }, - Object { - "x": 1593860400000, - "y": null, - }, - Object { - "x": 1593861000000, - "y": null, - }, - Object { - "x": 1593861600000, - "y": null, - }, - Object { - "x": 1593862200000, - "y": null, - }, - Object { - "x": 1593862800000, - "y": null, - }, - Object { - "x": 1593863400000, - "y": null, - }, - Object { - "x": 1593864000000, - "y": null, - }, - Object { - "x": 1593864600000, - "y": null, - }, - Object { - "x": 1593865200000, - "y": null, - }, - Object { - "x": 1593865800000, - "y": null, - }, - Object { - "x": 1593866400000, - "y": null, - }, - Object { - "x": 1593867000000, - "y": null, - }, - Object { - "x": 1593867600000, - "y": null, - }, - Object { - "x": 1593868200000, - "y": null, - }, - Object { - "x": 1593868800000, - "y": null, - }, - Object { - "x": 1593869400000, - "y": null, - }, - Object { - "x": 1593870000000, - "y": null, - }, - Object { - "x": 1593870600000, - "y": null, - }, - Object { - "x": 1593871200000, - "y": null, - }, - Object { - "x": 1593871800000, - "y": null, - }, - Object { - "x": 1593872400000, - "y": null, - }, - Object { - "x": 1593873000000, - "y": null, - }, - Object { - "x": 1593873600000, - "y": null, - }, - Object { - "x": 1593874200000, - "y": null, - }, - Object { - "x": 1593874800000, - "y": null, - }, - Object { - "x": 1593875400000, - "y": null, - }, - Object { - "x": 1593876000000, - "y": null, - }, - Object { - "x": 1593876600000, - "y": null, - }, - Object { - "x": 1593877200000, - "y": null, - }, - Object { - "x": 1593877800000, - "y": null, - }, - Object { - "x": 1593878400000, - "y": null, - }, - Object { - "x": 1593879000000, - "y": null, - }, - Object { - "x": 1593879600000, - "y": null, - }, - Object { - "x": 1593880200000, - "y": null, - }, - Object { - "x": 1593880800000, - "y": null, - }, - Object { - "x": 1593881400000, - "y": null, - }, - Object { - "x": 1593882000000, - "y": null, - }, - Object { - "x": 1593882600000, - "y": null, - }, - Object { - "x": 1593883200000, - "y": null, - }, - Object { - "x": 1593883800000, - "y": null, - }, - Object { - "x": 1593884400000, - "y": null, - }, - Object { - "x": 1593885000000, - "y": null, - }, - Object { - "x": 1593885600000, - "y": null, - }, - Object { - "x": 1593886200000, - "y": null, - }, - Object { - "x": 1593886800000, - "y": null, - }, - Object { - "x": 1593887400000, - "y": null, - }, - Object { - "x": 1593888000000, - "y": null, - }, - Object { - "x": 1593888600000, - "y": 114680, - }, - Object { - "x": 1593889200000, - "y": 659448, - }, - Object { - "x": 1593889800000, - "y": 122360, - }, - Object { - "x": 1593890400000, - "y": 121336, - }, - Object { - "x": 1593891000000, - "y": 120828, - }, - Object { - "x": 1593891600000, - "y": 139256, - }, - Object { - "x": 1593892200000, - "y": 76792, - }, - Object { - "x": 1593892800000, - "y": 129528, - }, - Object { - "x": 1593893400000, - "y": 378872, - }, - Object { - "x": 1593894000000, - "y": 97272, - }, - Object { - "x": 1593894600000, - "y": 102904, - }, - Object { - "x": 1593895200000, - "y": 100856, - }, - Object { - "x": 1593895800000, - "y": 97784, - }, - Object { - "x": 1593896400000, - "y": 72700, - }, - Object { - "x": 1593897000000, - "y": 98296, - }, - Object { - "x": 1593897600000, - "y": 112120, - }, - Object { - "x": 1593898200000, - "y": 91640, - }, - Object { - "x": 1593898800000, - "y": 83448, - }, - Object { - "x": 1593899400000, - "y": 84476, - }, - Object { - "x": 1593900000000, - "y": 117756, - }, - Object { - "x": 1593900600000, - "y": 66556, - }, - Object { - "x": 1593901200000, - "y": 130552, - }, - Object { - "x": 1593901800000, - "y": 111608, - }, - Object { - "x": 1593902400000, - "y": null, - }, - Object { - "x": 1593903000000, - "y": null, - }, - Object { - "x": 1593903600000, - "y": 276448, - }, - Object { - "x": 1593904200000, - "y": null, - }, - Object { - "x": 1593904800000, - "y": null, - }, - Object { - "x": 1593905400000, - "y": null, - }, - Object { - "x": 1593906000000, - "y": 1028088, - }, - Object { - "x": 1593906600000, - "y": null, - }, - Object { - "x": 1593907200000, - "y": null, - }, - Object { - "x": 1593907800000, - "y": null, - }, - Object { - "x": 1593908400000, - "y": null, - }, - Object { - "x": 1593909000000, - "y": null, - }, - Object { - "x": 1593909600000, - "y": null, - }, - Object { - "x": 1593910200000, - "y": 352128, - }, - Object { - "x": 1593910800000, - "y": null, - }, - Object { - "x": 1593911400000, - "y": null, - }, - Object { - "x": 1593912000000, - "y": null, - }, - Object { - "x": 1593912600000, - "y": null, - }, - Object { - "x": 1593913200000, - "y": null, - }, - Object { - "x": 1593913800000, - "y": null, - }, - Object { - "x": 1593914400000, - "y": null, - }, - Object { - "x": 1593915000000, - "y": null, - }, - Object { - "x": 1593915600000, - "y": null, - }, - Object { - "x": 1593916200000, - "y": null, - }, - Object { - "x": 1593916800000, - "y": 348144, - }, - Object { - "x": 1593917400000, - "y": null, - }, - Object { - "x": 1593918000000, - "y": null, - }, - Object { - "x": 1593918600000, - "y": null, - }, - Object { - "x": 1593919200000, - "y": null, - }, - Object { - "x": 1593919800000, - "y": null, - }, - Object { - "x": 1593920400000, - "y": null, - }, - Object { - "x": 1593921000000, - "y": null, - }, - Object { - "x": 1593921600000, - "y": null, - }, - Object { - "x": 1593922200000, - "y": null, - }, - Object { - "x": 1593922800000, - "y": null, - }, - Object { - "x": 1593923400000, - "y": 270328, - }, - Object { - "x": 1593924000000, - "y": null, - }, - Object { - "x": 1593924600000, - "y": null, - }, - Object { - "x": 1593925200000, - "y": null, - }, - Object { - "x": 1593925800000, - "y": null, - }, - Object { - "x": 1593926400000, - "y": null, - }, - Object { - "x": 1593927000000, - "y": null, - }, - Object { - "x": 1593927600000, - "y": null, - }, - Object { - "x": 1593928200000, - "y": null, - }, - Object { - "x": 1593928800000, - "y": null, - }, - Object { - "x": 1593929400000, - "y": null, - }, - Object { - "x": 1593930000000, - "y": 1687544, - }, - Object { - "x": 1593930600000, - "y": null, - }, - Object { - "x": 1593931200000, - "y": null, - }, - Object { - "x": 1593931800000, - "y": null, - }, - Object { - "x": 1593932400000, - "y": 798656, - }, - Object { - "x": 1593933000000, - "y": null, - }, - Object { - "x": 1593933600000, - "y": null, - }, - Object { - "x": 1593934200000, - "y": null, - }, - Object { - "x": 1593934800000, - "y": null, - }, - Object { - "x": 1593935400000, - "y": null, - }, - Object { - "x": 1593936000000, - "y": null, - }, - Object { - "x": 1593936600000, - "y": 3653624, - }, - Object { - "x": 1593937200000, - "y": 3276768, - }, - Object { - "x": 1593937800000, - "y": 522208, - }, - Object { - "x": 1593938400000, - "y": 372728, - }, - ], - "p99": Array [ - Object { - "x": 1593852000000, - "y": null, - }, - Object { - "x": 1593852600000, - "y": null, - }, - Object { - "x": 1593853200000, - "y": null, - }, - Object { - "x": 1593853800000, - "y": null, - }, - Object { - "x": 1593854400000, - "y": null, - }, - Object { - "x": 1593855000000, - "y": null, - }, - Object { - "x": 1593855600000, - "y": null, - }, - Object { - "x": 1593856200000, - "y": null, - }, - Object { - "x": 1593856800000, - "y": null, - }, - Object { - "x": 1593857400000, - "y": null, - }, - Object { - "x": 1593858000000, - "y": null, - }, - Object { - "x": 1593858600000, - "y": null, - }, - Object { - "x": 1593859200000, - "y": null, - }, - Object { - "x": 1593859800000, - "y": null, - }, - Object { - "x": 1593860400000, - "y": null, - }, - Object { - "x": 1593861000000, - "y": null, - }, - Object { - "x": 1593861600000, - "y": null, - }, - Object { - "x": 1593862200000, - "y": null, - }, - Object { - "x": 1593862800000, - "y": null, - }, - Object { - "x": 1593863400000, - "y": null, - }, - Object { - "x": 1593864000000, - "y": null, - }, - Object { - "x": 1593864600000, - "y": null, - }, - Object { - "x": 1593865200000, - "y": null, - }, - Object { - "x": 1593865800000, - "y": null, - }, - Object { - "x": 1593866400000, - "y": null, - }, - Object { - "x": 1593867000000, - "y": null, - }, - Object { - "x": 1593867600000, - "y": null, - }, - Object { - "x": 1593868200000, - "y": null, - }, - Object { - "x": 1593868800000, - "y": null, - }, - Object { - "x": 1593869400000, - "y": null, - }, - Object { - "x": 1593870000000, - "y": null, - }, - Object { - "x": 1593870600000, - "y": null, - }, - Object { - "x": 1593871200000, - "y": null, - }, - Object { - "x": 1593871800000, - "y": null, - }, - Object { - "x": 1593872400000, - "y": null, - }, - Object { - "x": 1593873000000, - "y": null, - }, - Object { - "x": 1593873600000, - "y": null, - }, - Object { - "x": 1593874200000, - "y": null, - }, - Object { - "x": 1593874800000, - "y": null, - }, - Object { - "x": 1593875400000, - "y": null, - }, - Object { - "x": 1593876000000, - "y": null, - }, - Object { - "x": 1593876600000, - "y": null, - }, - Object { - "x": 1593877200000, - "y": null, - }, - Object { - "x": 1593877800000, - "y": null, - }, - Object { - "x": 1593878400000, - "y": null, - }, - Object { - "x": 1593879000000, - "y": null, - }, - Object { - "x": 1593879600000, - "y": null, - }, - Object { - "x": 1593880200000, - "y": null, - }, - Object { - "x": 1593880800000, - "y": null, - }, - Object { - "x": 1593881400000, - "y": null, - }, - Object { - "x": 1593882000000, - "y": null, - }, - Object { - "x": 1593882600000, - "y": null, - }, - Object { - "x": 1593883200000, - "y": null, - }, - Object { - "x": 1593883800000, - "y": null, - }, - Object { - "x": 1593884400000, - "y": null, - }, - Object { - "x": 1593885000000, - "y": null, - }, - Object { - "x": 1593885600000, - "y": null, - }, - Object { - "x": 1593886200000, - "y": null, - }, - Object { - "x": 1593886800000, - "y": null, - }, - Object { - "x": 1593887400000, - "y": null, - }, - Object { - "x": 1593888000000, - "y": null, - }, - Object { - "x": 1593888600000, - "y": 827384, - }, - Object { - "x": 1593889200000, - "y": 2326520, - }, - Object { - "x": 1593889800000, - "y": 1130488, - }, - Object { - "x": 1593890400000, - "y": 1032184, - }, - Object { - "x": 1593891000000, - "y": 770044, - }, - Object { - "x": 1593891600000, - "y": 651256, - }, - Object { - "x": 1593892200000, - "y": 667640, - }, - Object { - "x": 1593892800000, - "y": 708600, - }, - Object { - "x": 1593893400000, - "y": 815096, - }, - Object { - "x": 1593894000000, - "y": 688120, - }, - Object { - "x": 1593894600000, - "y": 978936, - }, - Object { - "x": 1593895200000, - "y": 839672, - }, - Object { - "x": 1593895800000, - "y": 757752, - }, - Object { - "x": 1593896400000, - "y": 577532, - }, - Object { - "x": 1593897000000, - "y": 618488, - }, - Object { - "x": 1593897600000, - "y": 565240, - }, - Object { - "x": 1593898200000, - "y": 618488, - }, - Object { - "x": 1593898800000, - "y": 655352, - }, - Object { - "x": 1593899400000, - "y": 843772, - }, - Object { - "x": 1593900000000, - "y": 831484, - }, - Object { - "x": 1593900600000, - "y": 430076, - }, - Object { - "x": 1593901200000, - "y": 864248, - }, - Object { - "x": 1593901800000, - "y": 655352, - }, - Object { - "x": 1593902400000, - "y": null, - }, - Object { - "x": 1593903000000, - "y": null, - }, - Object { - "x": 1593903600000, - "y": 2883552, - }, - Object { - "x": 1593904200000, - "y": null, - }, - Object { - "x": 1593904800000, - "y": null, - }, - Object { - "x": 1593905400000, - "y": null, - }, - Object { - "x": 1593906000000, - "y": 6094840, - }, - Object { - "x": 1593906600000, - "y": null, - }, - Object { - "x": 1593907200000, - "y": null, - }, - Object { - "x": 1593907800000, - "y": null, - }, - Object { - "x": 1593908400000, - "y": null, - }, - Object { - "x": 1593909000000, - "y": null, - }, - Object { - "x": 1593909600000, - "y": null, - }, - Object { - "x": 1593910200000, - "y": 446336, - }, - Object { - "x": 1593910800000, - "y": null, - }, - Object { - "x": 1593911400000, - "y": null, - }, - Object { - "x": 1593912000000, - "y": null, - }, - Object { - "x": 1593912600000, - "y": null, - }, - Object { - "x": 1593913200000, - "y": null, - }, - Object { - "x": 1593913800000, - "y": null, - }, - Object { - "x": 1593914400000, - "y": null, - }, - Object { - "x": 1593915000000, - "y": null, - }, - Object { - "x": 1593915600000, - "y": null, - }, - Object { - "x": 1593916200000, - "y": null, - }, - Object { - "x": 1593916800000, - "y": 3293168, - }, - Object { - "x": 1593917400000, - "y": null, - }, - Object { - "x": 1593918000000, - "y": null, - }, - Object { - "x": 1593918600000, - "y": null, - }, - Object { - "x": 1593919200000, - "y": null, - }, - Object { - "x": 1593919800000, - "y": null, - }, - Object { - "x": 1593920400000, - "y": null, - }, - Object { - "x": 1593921000000, - "y": null, - }, - Object { - "x": 1593921600000, - "y": null, - }, - Object { - "x": 1593922200000, - "y": null, - }, - Object { - "x": 1593922800000, - "y": null, - }, - Object { - "x": 1593923400000, - "y": 299000, - }, - Object { - "x": 1593924000000, - "y": null, - }, - Object { - "x": 1593924600000, - "y": null, - }, - Object { - "x": 1593925200000, - "y": null, - }, - Object { - "x": 1593925800000, - "y": null, - }, - Object { - "x": 1593926400000, - "y": null, - }, - Object { - "x": 1593927000000, - "y": null, - }, - Object { - "x": 1593927600000, - "y": null, - }, - Object { - "x": 1593928200000, - "y": null, - }, - Object { - "x": 1593928800000, - "y": null, - }, - Object { - "x": 1593929400000, - "y": null, - }, - Object { - "x": 1593930000000, - "y": 5046264, - }, - Object { - "x": 1593930600000, - "y": null, - }, - Object { - "x": 1593931200000, - "y": null, - }, - Object { - "x": 1593931800000, - "y": null, - }, - Object { - "x": 1593932400000, - "y": 4292544, - }, - Object { - "x": 1593933000000, - "y": null, - }, - Object { - "x": 1593933600000, - "y": null, - }, - Object { - "x": 1593934200000, - "y": null, - }, - Object { - "x": 1593934800000, - "y": null, - }, - Object { - "x": 1593935400000, - "y": null, - }, - Object { - "x": 1593936000000, - "y": null, - }, - Object { - "x": 1593936600000, - "y": 5046264, - }, - Object { - "x": 1593937200000, - "y": 4292576, - }, - Object { - "x": 1593937800000, - "y": 4128736, - }, - Object { - "x": 1593938400000, - "y": 843768, - }, - ], - }, - "tpmBuckets": Array [ - Object { - "avg": 1215, - "dataPoints": Array [ - Object { - "x": 1593852000000, - "y": 0, - }, - Object { - "x": 1593852600000, - "y": 0, - }, - Object { - "x": 1593853200000, - "y": 0, - }, - Object { - "x": 1593853800000, - "y": 0, - }, - Object { - "x": 1593854400000, - "y": 0, - }, - Object { - "x": 1593855000000, - "y": 0, - }, - Object { - "x": 1593855600000, - "y": 0, - }, - Object { - "x": 1593856200000, - "y": 0, - }, - Object { - "x": 1593856800000, - "y": 0, - }, - Object { - "x": 1593857400000, - "y": 0, - }, - Object { - "x": 1593858000000, - "y": 0, - }, - Object { - "x": 1593858600000, - "y": 0, - }, - Object { - "x": 1593859200000, - "y": 0, - }, - Object { - "x": 1593859800000, - "y": 0, - }, - Object { - "x": 1593860400000, - "y": 0, - }, - Object { - "x": 1593861000000, - "y": 0, - }, - Object { - "x": 1593861600000, - "y": 0, - }, - Object { - "x": 1593862200000, - "y": 0, - }, - Object { - "x": 1593862800000, - "y": 0, - }, - Object { - "x": 1593863400000, - "y": 0, - }, - Object { - "x": 1593864000000, - "y": 0, - }, - Object { - "x": 1593864600000, - "y": 0, - }, - Object { - "x": 1593865200000, - "y": 0, - }, - Object { - "x": 1593865800000, - "y": 0, - }, - Object { - "x": 1593866400000, - "y": 0, - }, - Object { - "x": 1593867000000, - "y": 0, - }, - Object { - "x": 1593867600000, - "y": 0, - }, - Object { - "x": 1593868200000, - "y": 0, - }, - Object { - "x": 1593868800000, - "y": 0, - }, - Object { - "x": 1593869400000, - "y": 0, - }, - Object { - "x": 1593870000000, - "y": 0, - }, - Object { - "x": 1593870600000, - "y": 0, - }, - Object { - "x": 1593871200000, - "y": 0, - }, - Object { - "x": 1593871800000, - "y": 0, - }, - Object { - "x": 1593872400000, - "y": 0, - }, - Object { - "x": 1593873000000, - "y": 0, - }, - Object { - "x": 1593873600000, - "y": 0, - }, - Object { - "x": 1593874200000, - "y": 0, - }, - Object { - "x": 1593874800000, - "y": 0, - }, - Object { - "x": 1593875400000, - "y": 0, - }, - Object { - "x": 1593876000000, - "y": 0, - }, - Object { - "x": 1593876600000, - "y": 0, - }, - Object { - "x": 1593877200000, - "y": 0, - }, - Object { - "x": 1593877800000, - "y": 0, - }, - Object { - "x": 1593878400000, - "y": 0, - }, - Object { - "x": 1593879000000, - "y": 0, - }, - Object { - "x": 1593879600000, - "y": 0, - }, - Object { - "x": 1593880200000, - "y": 0, - }, - Object { - "x": 1593880800000, - "y": 0, - }, - Object { - "x": 1593881400000, - "y": 0, - }, - Object { - "x": 1593882000000, - "y": 0, - }, - Object { - "x": 1593882600000, - "y": 0, - }, - Object { - "x": 1593883200000, - "y": 0, - }, - Object { - "x": 1593883800000, - "y": 0, - }, - Object { - "x": 1593884400000, - "y": 0, - }, - Object { - "x": 1593885000000, - "y": 0, - }, - Object { - "x": 1593885600000, - "y": 0, - }, - Object { - "x": 1593886200000, - "y": 0, - }, - Object { - "x": 1593886800000, - "y": 0, - }, - Object { - "x": 1593887400000, - "y": 0, - }, - Object { - "x": 1593888000000, - "y": 0, - }, - Object { - "x": 1593888600000, - "y": 84.5, - }, - Object { - "x": 1593889200000, - "y": 222, - }, - Object { - "x": 1593889800000, - "y": 230, - }, - Object { - "x": 1593890400000, - "y": 253, - }, - Object { - "x": 1593891000000, - "y": 239.5, - }, - Object { - "x": 1593891600000, - "y": 228.5, - }, - Object { - "x": 1593892200000, - "y": 257, - }, - Object { - "x": 1593892800000, - "y": 241, - }, - Object { - "x": 1593893400000, - "y": 252, - }, - Object { - "x": 1593894000000, - "y": 266, - }, - Object { - "x": 1593894600000, - "y": 229, - }, - Object { - "x": 1593895200000, - "y": 224, - }, - Object { - "x": 1593895800000, - "y": 234, - }, - Object { - "x": 1593896400000, - "y": 263, - }, - Object { - "x": 1593897000000, - "y": 247.5, - }, - Object { - "x": 1593897600000, - "y": 246, - }, - Object { - "x": 1593898200000, - "y": 243.5, - }, - Object { - "x": 1593898800000, - "y": 245.5, - }, - Object { - "x": 1593899400000, - "y": 243, - }, - Object { - "x": 1593900000000, - "y": 229, - }, - Object { - "x": 1593900600000, - "y": 264, - }, - Object { - "x": 1593901200000, - "y": 233.5, - }, - Object { - "x": 1593901800000, - "y": 89.5, - }, - Object { - "x": 1593902400000, - "y": 0, - }, - Object { - "x": 1593903000000, - "y": 0, - }, - Object { - "x": 1593903600000, - "y": 19.5, - }, - Object { - "x": 1593904200000, - "y": 0, - }, - Object { - "x": 1593904800000, - "y": 0, - }, - Object { - "x": 1593905400000, - "y": 0, - }, - Object { - "x": 1593906000000, - "y": 18, - }, - Object { - "x": 1593906600000, - "y": 0, - }, - Object { - "x": 1593907200000, - "y": 0, - }, - Object { - "x": 1593907800000, - "y": 0, - }, - Object { - "x": 1593908400000, - "y": 0, - }, - Object { - "x": 1593909000000, - "y": 0, - }, - Object { - "x": 1593909600000, - "y": 0, - }, - Object { - "x": 1593910200000, - "y": 17, - }, - Object { - "x": 1593910800000, - "y": 0, - }, - Object { - "x": 1593911400000, - "y": 0, - }, - Object { - "x": 1593912000000, - "y": 0, - }, - Object { - "x": 1593912600000, - "y": 0, - }, - Object { - "x": 1593913200000, - "y": 0, - }, - Object { - "x": 1593913800000, - "y": 0, - }, - Object { - "x": 1593914400000, - "y": 0, - }, - Object { - "x": 1593915000000, - "y": 0, - }, - Object { - "x": 1593915600000, - "y": 0, - }, - Object { - "x": 1593916200000, - "y": 0, - }, - Object { - "x": 1593916800000, - "y": 15.5, - }, - Object { - "x": 1593917400000, - "y": 0, - }, - Object { - "x": 1593918000000, - "y": 0, - }, - Object { - "x": 1593918600000, - "y": 0, - }, - Object { - "x": 1593919200000, - "y": 0, - }, - Object { - "x": 1593919800000, - "y": 0, - }, - Object { - "x": 1593920400000, - "y": 0, - }, - Object { - "x": 1593921000000, - "y": 0, - }, - Object { - "x": 1593921600000, - "y": 0, - }, - Object { - "x": 1593922200000, - "y": 0, - }, - Object { - "x": 1593922800000, - "y": 0, - }, - Object { - "x": 1593923400000, - "y": 24.5, - }, - Object { - "x": 1593924000000, - "y": 0, - }, - Object { - "x": 1593924600000, - "y": 0, - }, - Object { - "x": 1593925200000, - "y": 0, - }, - Object { - "x": 1593925800000, - "y": 0, - }, - Object { - "x": 1593926400000, - "y": 0, - }, - Object { - "x": 1593927000000, - "y": 0, - }, - Object { - "x": 1593927600000, - "y": 0, - }, - Object { - "x": 1593928200000, - "y": 0, - }, - Object { - "x": 1593928800000, - "y": 0, - }, - Object { - "x": 1593929400000, - "y": 0, - }, - Object { - "x": 1593930000000, - "y": 25, - }, - Object { - "x": 1593930600000, - "y": 0, - }, - Object { - "x": 1593931200000, - "y": 0, - }, - Object { - "x": 1593931800000, - "y": 0, - }, - Object { - "x": 1593932400000, - "y": 18.5, - }, - Object { - "x": 1593933000000, - "y": 0, - }, - Object { - "x": 1593933600000, - "y": 0, - }, - Object { - "x": 1593934200000, - "y": 0, - }, - Object { - "x": 1593934800000, - "y": 0, - }, - Object { - "x": 1593935400000, - "y": 0, - }, - Object { - "x": 1593936000000, - "y": 0, - }, - Object { - "x": 1593936600000, - "y": 97, - }, - Object { - "x": 1593937200000, - "y": 192.5, - }, - Object { - "x": 1593937800000, - "y": 210.5, - }, - Object { - "x": 1593938400000, - "y": 172, - }, - ], - "key": "HTTP 2xx", - }, - Object { - "avg": 382.8, - "dataPoints": Array [ - Object { - "x": 1593852000000, - "y": 0, - }, - Object { - "x": 1593852600000, - "y": 0, - }, - Object { - "x": 1593853200000, - "y": 0, - }, - Object { - "x": 1593853800000, - "y": 0, - }, - Object { - "x": 1593854400000, - "y": 0, - }, - Object { - "x": 1593855000000, - "y": 0, - }, - Object { - "x": 1593855600000, - "y": 0, - }, - Object { - "x": 1593856200000, - "y": 0, - }, - Object { - "x": 1593856800000, - "y": 0, - }, - Object { - "x": 1593857400000, - "y": 0, - }, - Object { - "x": 1593858000000, - "y": 0, - }, - Object { - "x": 1593858600000, - "y": 0, - }, - Object { - "x": 1593859200000, - "y": 0, - }, - Object { - "x": 1593859800000, - "y": 0, - }, - Object { - "x": 1593860400000, - "y": 0, - }, - Object { - "x": 1593861000000, - "y": 0, - }, - Object { - "x": 1593861600000, - "y": 0, - }, - Object { - "x": 1593862200000, - "y": 0, - }, - Object { - "x": 1593862800000, - "y": 0, - }, - Object { - "x": 1593863400000, - "y": 0, - }, - Object { - "x": 1593864000000, - "y": 0, - }, - Object { - "x": 1593864600000, - "y": 0, - }, - Object { - "x": 1593865200000, - "y": 0, - }, - Object { - "x": 1593865800000, - "y": 0, - }, - Object { - "x": 1593866400000, - "y": 0, - }, - Object { - "x": 1593867000000, - "y": 0, - }, - Object { - "x": 1593867600000, - "y": 0, - }, - Object { - "x": 1593868200000, - "y": 0, - }, - Object { - "x": 1593868800000, - "y": 0, - }, - Object { - "x": 1593869400000, - "y": 0, - }, - Object { - "x": 1593870000000, - "y": 0, - }, - Object { - "x": 1593870600000, - "y": 0, - }, - Object { - "x": 1593871200000, - "y": 0, - }, - Object { - "x": 1593871800000, - "y": 0, - }, - Object { - "x": 1593872400000, - "y": 0, - }, - Object { - "x": 1593873000000, - "y": 0, - }, - Object { - "x": 1593873600000, - "y": 0, - }, - Object { - "x": 1593874200000, - "y": 0, - }, - Object { - "x": 1593874800000, - "y": 0, - }, - Object { - "x": 1593875400000, - "y": 0, - }, - Object { - "x": 1593876000000, - "y": 0, - }, - Object { - "x": 1593876600000, - "y": 0, - }, - Object { - "x": 1593877200000, - "y": 0, - }, - Object { - "x": 1593877800000, - "y": 0, - }, - Object { - "x": 1593878400000, - "y": 0, - }, - Object { - "x": 1593879000000, - "y": 0, - }, - Object { - "x": 1593879600000, - "y": 0, - }, - Object { - "x": 1593880200000, - "y": 0, - }, - Object { - "x": 1593880800000, - "y": 0, - }, - Object { - "x": 1593881400000, - "y": 0, - }, - Object { - "x": 1593882000000, - "y": 0, - }, - Object { - "x": 1593882600000, - "y": 0, - }, - Object { - "x": 1593883200000, - "y": 0, - }, - Object { - "x": 1593883800000, - "y": 0, - }, - Object { - "x": 1593884400000, - "y": 0, - }, - Object { - "x": 1593885000000, - "y": 0, - }, - Object { - "x": 1593885600000, - "y": 0, - }, - Object { - "x": 1593886200000, - "y": 0, - }, - Object { - "x": 1593886800000, - "y": 0, - }, - Object { - "x": 1593887400000, - "y": 0, - }, - Object { - "x": 1593888000000, - "y": 0, - }, - Object { - "x": 1593888600000, - "y": 31, - }, - Object { - "x": 1593889200000, - "y": 26, - }, - Object { - "x": 1593889800000, - "y": 64, - }, - Object { - "x": 1593890400000, - "y": 71.5, - }, - Object { - "x": 1593891000000, - "y": 64.5, - }, - Object { - "x": 1593891600000, - "y": 60.5, - }, - Object { - "x": 1593892200000, - "y": 146, - }, - Object { - "x": 1593892800000, - "y": 69.5, - }, - Object { - "x": 1593893400000, - "y": 52, - }, - Object { - "x": 1593894000000, - "y": 99, - }, - Object { - "x": 1593894600000, - "y": 89.5, - }, - Object { - "x": 1593895200000, - "y": 58.5, - }, - Object { - "x": 1593895800000, - "y": 91.5, - }, - Object { - "x": 1593896400000, - "y": 132, - }, - Object { - "x": 1593897000000, - "y": 90, - }, - Object { - "x": 1593897600000, - "y": 80, - }, - Object { - "x": 1593898200000, - "y": 104, - }, - Object { - "x": 1593898800000, - "y": 79, - }, - Object { - "x": 1593899400000, - "y": 88, - }, - Object { - "x": 1593900000000, - "y": 91.5, - }, - Object { - "x": 1593900600000, - "y": 117, - }, - Object { - "x": 1593901200000, - "y": 62.5, - }, - Object { - "x": 1593901800000, - "y": 24, - }, - Object { - "x": 1593902400000, - "y": 0, - }, - Object { - "x": 1593903000000, - "y": 0, - }, - Object { - "x": 1593903600000, - "y": 4.5, - }, - Object { - "x": 1593904200000, - "y": 0, - }, - Object { - "x": 1593904800000, - "y": 0, - }, - Object { - "x": 1593905400000, - "y": 0, - }, - Object { - "x": 1593906000000, - "y": 0, - }, - Object { - "x": 1593906600000, - "y": 0, - }, - Object { - "x": 1593907200000, - "y": 0, - }, - Object { - "x": 1593907800000, - "y": 0, - }, - Object { - "x": 1593908400000, - "y": 0, - }, - Object { - "x": 1593909000000, - "y": 0, - }, - Object { - "x": 1593909600000, - "y": 0, - }, - Object { - "x": 1593910200000, - "y": 0, - }, - Object { - "x": 1593910800000, - "y": 0, - }, - Object { - "x": 1593911400000, - "y": 0, - }, - Object { - "x": 1593912000000, - "y": 0, - }, - Object { - "x": 1593912600000, - "y": 0, - }, - Object { - "x": 1593913200000, - "y": 0, - }, - Object { - "x": 1593913800000, - "y": 0, - }, - Object { - "x": 1593914400000, - "y": 0, - }, - Object { - "x": 1593915000000, - "y": 0, - }, - Object { - "x": 1593915600000, - "y": 0, - }, - Object { - "x": 1593916200000, - "y": 0, - }, - Object { - "x": 1593916800000, - "y": 0, - }, - Object { - "x": 1593917400000, - "y": 0, - }, - Object { - "x": 1593918000000, - "y": 0, - }, - Object { - "x": 1593918600000, - "y": 0, - }, - Object { - "x": 1593919200000, - "y": 0, - }, - Object { - "x": 1593919800000, - "y": 0, - }, - Object { - "x": 1593920400000, - "y": 0, - }, - Object { - "x": 1593921000000, - "y": 0, - }, - Object { - "x": 1593921600000, - "y": 0, - }, - Object { - "x": 1593922200000, - "y": 0, - }, - Object { - "x": 1593922800000, - "y": 0, - }, - Object { - "x": 1593923400000, - "y": 5.5, - }, - Object { - "x": 1593924000000, - "y": 0, - }, - Object { - "x": 1593924600000, - "y": 0, - }, - Object { - "x": 1593925200000, - "y": 0, - }, - Object { - "x": 1593925800000, - "y": 0, - }, - Object { - "x": 1593926400000, - "y": 0, - }, - Object { - "x": 1593927000000, - "y": 0, - }, - Object { - "x": 1593927600000, - "y": 0, - }, - Object { - "x": 1593928200000, - "y": 0, - }, - Object { - "x": 1593928800000, - "y": 0, - }, - Object { - "x": 1593929400000, - "y": 0, - }, - Object { - "x": 1593930000000, - "y": 14, - }, - Object { - "x": 1593930600000, - "y": 0, - }, - Object { - "x": 1593931200000, - "y": 0, - }, - Object { - "x": 1593931800000, - "y": 0, - }, - Object { - "x": 1593932400000, - "y": 1, - }, - Object { - "x": 1593933000000, - "y": 0, - }, - Object { - "x": 1593933600000, - "y": 0, - }, - Object { - "x": 1593934200000, - "y": 0, - }, - Object { - "x": 1593934800000, - "y": 0, - }, - Object { - "x": 1593935400000, - "y": 0, - }, - Object { - "x": 1593936000000, - "y": 0, - }, - Object { - "x": 1593936600000, - "y": 0, - }, - Object { - "x": 1593937200000, - "y": 31.5, - }, - Object { - "x": 1593937800000, - "y": 25, - }, - Object { - "x": 1593938400000, - "y": 41, - }, - ], - "key": "HTTP 3xx", - }, - Object { - "avg": 68.3, - "dataPoints": Array [ - Object { - "x": 1593852000000, - "y": 0, - }, - Object { - "x": 1593852600000, - "y": 0, - }, - Object { - "x": 1593853200000, - "y": 0, - }, - Object { - "x": 1593853800000, - "y": 0, - }, - Object { - "x": 1593854400000, - "y": 0, - }, - Object { - "x": 1593855000000, - "y": 0, - }, - Object { - "x": 1593855600000, - "y": 0, - }, - Object { - "x": 1593856200000, - "y": 0, - }, - Object { - "x": 1593856800000, - "y": 0, - }, - Object { - "x": 1593857400000, - "y": 0, - }, - Object { - "x": 1593858000000, - "y": 0, - }, - Object { - "x": 1593858600000, - "y": 0, - }, - Object { - "x": 1593859200000, - "y": 0, - }, - Object { - "x": 1593859800000, - "y": 0, - }, - Object { - "x": 1593860400000, - "y": 0, - }, - Object { - "x": 1593861000000, - "y": 0, - }, - Object { - "x": 1593861600000, - "y": 0, - }, - Object { - "x": 1593862200000, - "y": 0, - }, - Object { - "x": 1593862800000, - "y": 0, - }, - Object { - "x": 1593863400000, - "y": 0, - }, - Object { - "x": 1593864000000, - "y": 0, - }, - Object { - "x": 1593864600000, - "y": 0, - }, - Object { - "x": 1593865200000, - "y": 0, - }, - Object { - "x": 1593865800000, - "y": 0, - }, - Object { - "x": 1593866400000, - "y": 0, - }, - Object { - "x": 1593867000000, - "y": 0, - }, - Object { - "x": 1593867600000, - "y": 0, - }, - Object { - "x": 1593868200000, - "y": 0, - }, - Object { - "x": 1593868800000, - "y": 0, - }, - Object { - "x": 1593869400000, - "y": 0, - }, - Object { - "x": 1593870000000, - "y": 0, - }, - Object { - "x": 1593870600000, - "y": 0, - }, - Object { - "x": 1593871200000, - "y": 0, - }, - Object { - "x": 1593871800000, - "y": 0, - }, - Object { - "x": 1593872400000, - "y": 0, - }, - Object { - "x": 1593873000000, - "y": 0, - }, - Object { - "x": 1593873600000, - "y": 0, - }, - Object { - "x": 1593874200000, - "y": 0, - }, - Object { - "x": 1593874800000, - "y": 0, - }, - Object { - "x": 1593875400000, - "y": 0, - }, - Object { - "x": 1593876000000, - "y": 0, - }, - Object { - "x": 1593876600000, - "y": 0, - }, - Object { - "x": 1593877200000, - "y": 0, - }, - Object { - "x": 1593877800000, - "y": 0, - }, - Object { - "x": 1593878400000, - "y": 0, - }, - Object { - "x": 1593879000000, - "y": 0, - }, - Object { - "x": 1593879600000, - "y": 0, - }, - Object { - "x": 1593880200000, - "y": 0, - }, - Object { - "x": 1593880800000, - "y": 0, - }, - Object { - "x": 1593881400000, - "y": 0, - }, - Object { - "x": 1593882000000, - "y": 0, - }, - Object { - "x": 1593882600000, - "y": 0, - }, - Object { - "x": 1593883200000, - "y": 0, - }, - Object { - "x": 1593883800000, - "y": 0, - }, - Object { - "x": 1593884400000, - "y": 0, - }, - Object { - "x": 1593885000000, - "y": 0, - }, - Object { - "x": 1593885600000, - "y": 0, - }, - Object { - "x": 1593886200000, - "y": 0, - }, - Object { - "x": 1593886800000, - "y": 0, - }, - Object { - "x": 1593887400000, - "y": 0, - }, - Object { - "x": 1593888000000, - "y": 0, - }, - Object { - "x": 1593888600000, - "y": 5.5, - }, - Object { - "x": 1593889200000, - "y": 15.5, - }, - Object { - "x": 1593889800000, - "y": 9.5, - }, - Object { - "x": 1593890400000, - "y": 11.5, - }, - Object { - "x": 1593891000000, - "y": 13, - }, - Object { - "x": 1593891600000, - "y": 13.5, - }, - Object { - "x": 1593892200000, - "y": 13.5, - }, - Object { - "x": 1593892800000, - "y": 15, - }, - Object { - "x": 1593893400000, - "y": 14, - }, - Object { - "x": 1593894000000, - "y": 16.5, - }, - Object { - "x": 1593894600000, - "y": 11.5, - }, - Object { - "x": 1593895200000, - "y": 17.5, - }, - Object { - "x": 1593895800000, - "y": 13, - }, - Object { - "x": 1593896400000, - "y": 17.5, - }, - Object { - "x": 1593897000000, - "y": 12.5, - }, - Object { - "x": 1593897600000, - "y": 13, - }, - Object { - "x": 1593898200000, - "y": 12.5, - }, - Object { - "x": 1593898800000, - "y": 8.5, - }, - Object { - "x": 1593899400000, - "y": 9.5, - }, - Object { - "x": 1593900000000, - "y": 14, - }, - Object { - "x": 1593900600000, - "y": 12, - }, - Object { - "x": 1593901200000, - "y": 15, - }, - Object { - "x": 1593901800000, - "y": 3, - }, - Object { - "x": 1593902400000, - "y": 0, - }, - Object { - "x": 1593903000000, - "y": 0, - }, - Object { - "x": 1593903600000, - "y": 1, - }, - Object { - "x": 1593904200000, - "y": 0, - }, - Object { - "x": 1593904800000, - "y": 0, - }, - Object { - "x": 1593905400000, - "y": 0, - }, - Object { - "x": 1593906000000, - "y": 2, - }, - Object { - "x": 1593906600000, - "y": 0, - }, - Object { - "x": 1593907200000, - "y": 0, - }, - Object { - "x": 1593907800000, - "y": 0, - }, - Object { - "x": 1593908400000, - "y": 0, - }, - Object { - "x": 1593909000000, - "y": 0, - }, - Object { - "x": 1593909600000, - "y": 0, - }, - Object { - "x": 1593910200000, - "y": 0, - }, - Object { - "x": 1593910800000, - "y": 0, - }, - Object { - "x": 1593911400000, - "y": 0, - }, - Object { - "x": 1593912000000, - "y": 0, - }, - Object { - "x": 1593912600000, - "y": 0, - }, - Object { - "x": 1593913200000, - "y": 0, - }, - Object { - "x": 1593913800000, - "y": 0, - }, - Object { - "x": 1593914400000, - "y": 0, - }, - Object { - "x": 1593915000000, - "y": 0, - }, - Object { - "x": 1593915600000, - "y": 0, - }, - Object { - "x": 1593916200000, - "y": 0, - }, - Object { - "x": 1593916800000, - "y": 1.5, - }, - Object { - "x": 1593917400000, - "y": 0, - }, - Object { - "x": 1593918000000, - "y": 0, - }, - Object { - "x": 1593918600000, - "y": 0, - }, - Object { - "x": 1593919200000, - "y": 0, - }, - Object { - "x": 1593919800000, - "y": 0, - }, - Object { - "x": 1593920400000, - "y": 0, - }, - Object { - "x": 1593921000000, - "y": 0, - }, - Object { - "x": 1593921600000, - "y": 0, - }, - Object { - "x": 1593922200000, - "y": 0, - }, - Object { - "x": 1593922800000, - "y": 0, - }, - Object { - "x": 1593923400000, - "y": 1, - }, - Object { - "x": 1593924000000, - "y": 0, - }, - Object { - "x": 1593924600000, - "y": 0, - }, - Object { - "x": 1593925200000, - "y": 0, - }, - Object { - "x": 1593925800000, - "y": 0, - }, - Object { - "x": 1593926400000, - "y": 0, - }, - Object { - "x": 1593927000000, - "y": 0, - }, - Object { - "x": 1593927600000, - "y": 0, - }, - Object { - "x": 1593928200000, - "y": 0, - }, - Object { - "x": 1593928800000, - "y": 0, - }, - Object { - "x": 1593929400000, - "y": 0, - }, - Object { - "x": 1593930000000, - "y": 1.5, - }, - Object { - "x": 1593930600000, - "y": 0, - }, - Object { - "x": 1593931200000, - "y": 0, - }, - Object { - "x": 1593931800000, - "y": 0, - }, - Object { - "x": 1593932400000, - "y": 1, - }, - Object { - "x": 1593933000000, - "y": 0, - }, - Object { - "x": 1593933600000, - "y": 0, - }, - Object { - "x": 1593934200000, - "y": 0, - }, - Object { - "x": 1593934800000, - "y": 0, - }, - Object { - "x": 1593935400000, - "y": 0, - }, - Object { - "x": 1593936000000, - "y": 0, - }, - Object { - "x": 1593936600000, - "y": 7.5, - }, - Object { - "x": 1593937200000, - "y": 14.5, - }, - Object { - "x": 1593937800000, - "y": 15.5, - }, - Object { - "x": 1593938400000, - "y": 9, - }, - ], - "key": "HTTP 4xx", - }, - Object { - "avg": 37.8, - "dataPoints": Array [ - Object { - "x": 1593852000000, - "y": 0, - }, - Object { - "x": 1593852600000, - "y": 0, - }, - Object { - "x": 1593853200000, - "y": 0, - }, - Object { - "x": 1593853800000, - "y": 0, - }, - Object { - "x": 1593854400000, - "y": 0, - }, - Object { - "x": 1593855000000, - "y": 0, - }, - Object { - "x": 1593855600000, - "y": 0, - }, - Object { - "x": 1593856200000, - "y": 0, - }, - Object { - "x": 1593856800000, - "y": 0, - }, - Object { - "x": 1593857400000, - "y": 0, - }, - Object { - "x": 1593858000000, - "y": 0, - }, - Object { - "x": 1593858600000, - "y": 0, - }, - Object { - "x": 1593859200000, - "y": 0, - }, - Object { - "x": 1593859800000, - "y": 0, - }, - Object { - "x": 1593860400000, - "y": 0, - }, - Object { - "x": 1593861000000, - "y": 0, - }, - Object { - "x": 1593861600000, - "y": 0, - }, - Object { - "x": 1593862200000, - "y": 0, - }, - Object { - "x": 1593862800000, - "y": 0, - }, - Object { - "x": 1593863400000, - "y": 0, - }, - Object { - "x": 1593864000000, - "y": 0, - }, - Object { - "x": 1593864600000, - "y": 0, - }, - Object { - "x": 1593865200000, - "y": 0, - }, - Object { - "x": 1593865800000, - "y": 0, - }, - Object { - "x": 1593866400000, - "y": 0, - }, - Object { - "x": 1593867000000, - "y": 0, - }, - Object { - "x": 1593867600000, - "y": 0, - }, - Object { - "x": 1593868200000, - "y": 0, - }, - Object { - "x": 1593868800000, - "y": 0, - }, - Object { - "x": 1593869400000, - "y": 0, - }, - Object { - "x": 1593870000000, - "y": 0, - }, - Object { - "x": 1593870600000, - "y": 0, - }, - Object { - "x": 1593871200000, - "y": 0, - }, - Object { - "x": 1593871800000, - "y": 0, - }, - Object { - "x": 1593872400000, - "y": 0, - }, - Object { - "x": 1593873000000, - "y": 0, - }, - Object { - "x": 1593873600000, - "y": 0, - }, - Object { - "x": 1593874200000, - "y": 0, - }, - Object { - "x": 1593874800000, - "y": 0, - }, - Object { - "x": 1593875400000, - "y": 0, - }, - Object { - "x": 1593876000000, - "y": 0, - }, - Object { - "x": 1593876600000, - "y": 0, - }, - Object { - "x": 1593877200000, - "y": 0, - }, - Object { - "x": 1593877800000, - "y": 0, - }, - Object { - "x": 1593878400000, - "y": 0, - }, - Object { - "x": 1593879000000, - "y": 0, - }, - Object { - "x": 1593879600000, - "y": 0, - }, - Object { - "x": 1593880200000, - "y": 0, - }, - Object { - "x": 1593880800000, - "y": 0, - }, - Object { - "x": 1593881400000, - "y": 0, - }, - Object { - "x": 1593882000000, - "y": 0, - }, - Object { - "x": 1593882600000, - "y": 0, - }, - Object { - "x": 1593883200000, - "y": 0, - }, - Object { - "x": 1593883800000, - "y": 0, - }, - Object { - "x": 1593884400000, - "y": 0, - }, - Object { - "x": 1593885000000, - "y": 0, - }, - Object { - "x": 1593885600000, - "y": 0, - }, - Object { - "x": 1593886200000, - "y": 0, - }, - Object { - "x": 1593886800000, - "y": 0, - }, - Object { - "x": 1593887400000, - "y": 0, - }, - Object { - "x": 1593888000000, - "y": 0, - }, - Object { - "x": 1593888600000, - "y": 2.5, - }, - Object { - "x": 1593889200000, - "y": 7.5, - }, - Object { - "x": 1593889800000, - "y": 6, - }, - Object { - "x": 1593890400000, - "y": 8, - }, - Object { - "x": 1593891000000, - "y": 6, - }, - Object { - "x": 1593891600000, - "y": 8, - }, - Object { - "x": 1593892200000, - "y": 11.5, - }, - Object { - "x": 1593892800000, - "y": 5, - }, - Object { - "x": 1593893400000, - "y": 5, - }, - Object { - "x": 1593894000000, - "y": 9, - }, - Object { - "x": 1593894600000, - "y": 5, - }, - Object { - "x": 1593895200000, - "y": 8.5, - }, - Object { - "x": 1593895800000, - "y": 6.5, - }, - Object { - "x": 1593896400000, - "y": 9, - }, - Object { - "x": 1593897000000, - "y": 8.5, - }, - Object { - "x": 1593897600000, - "y": 8.5, - }, - Object { - "x": 1593898200000, - "y": 5.5, - }, - Object { - "x": 1593898800000, - "y": 5, - }, - Object { - "x": 1593899400000, - "y": 9, - }, - Object { - "x": 1593900000000, - "y": 8, - }, - Object { - "x": 1593900600000, - "y": 6, - }, - Object { - "x": 1593901200000, - "y": 9, - }, - Object { - "x": 1593901800000, - "y": 4, - }, - Object { - "x": 1593902400000, - "y": 0, - }, - Object { - "x": 1593903000000, - "y": 0, - }, - Object { - "x": 1593903600000, - "y": 0, - }, - Object { - "x": 1593904200000, - "y": 0, - }, - Object { - "x": 1593904800000, - "y": 0, - }, - Object { - "x": 1593905400000, - "y": 0, - }, - Object { - "x": 1593906000000, - "y": 0.5, - }, - Object { - "x": 1593906600000, - "y": 0, - }, - Object { - "x": 1593907200000, - "y": 0, - }, - Object { - "x": 1593907800000, - "y": 0, - }, - Object { - "x": 1593908400000, - "y": 0, - }, - Object { - "x": 1593909000000, - "y": 0, - }, - Object { - "x": 1593909600000, - "y": 0, - }, - Object { - "x": 1593910200000, - "y": 1.5, - }, - Object { - "x": 1593910800000, - "y": 0, - }, - Object { - "x": 1593911400000, - "y": 0, - }, - Object { - "x": 1593912000000, - "y": 0, - }, - Object { - "x": 1593912600000, - "y": 0, - }, - Object { - "x": 1593913200000, - "y": 0, - }, - Object { - "x": 1593913800000, - "y": 0, - }, - Object { - "x": 1593914400000, - "y": 0, - }, - Object { - "x": 1593915000000, - "y": 0, - }, - Object { - "x": 1593915600000, - "y": 0, - }, - Object { - "x": 1593916200000, - "y": 0, - }, - Object { - "x": 1593916800000, - "y": 1.5, - }, - Object { - "x": 1593917400000, - "y": 0, - }, - Object { - "x": 1593918000000, - "y": 0, - }, - Object { - "x": 1593918600000, - "y": 0, - }, - Object { - "x": 1593919200000, - "y": 0, - }, - Object { - "x": 1593919800000, - "y": 0, - }, - Object { - "x": 1593920400000, - "y": 0, - }, - Object { - "x": 1593921000000, - "y": 0, - }, - Object { - "x": 1593921600000, - "y": 0, - }, - Object { - "x": 1593922200000, - "y": 0, - }, - Object { - "x": 1593922800000, - "y": 0, - }, - Object { - "x": 1593923400000, - "y": 1, - }, - Object { - "x": 1593924000000, - "y": 0, - }, - Object { - "x": 1593924600000, - "y": 0, - }, - Object { - "x": 1593925200000, - "y": 0, - }, - Object { - "x": 1593925800000, - "y": 0, - }, - Object { - "x": 1593926400000, - "y": 0, - }, - Object { - "x": 1593927000000, - "y": 0, - }, - Object { - "x": 1593927600000, - "y": 0, - }, - Object { - "x": 1593928200000, - "y": 0, - }, - Object { - "x": 1593928800000, - "y": 0, - }, - Object { - "x": 1593929400000, - "y": 0, - }, - Object { - "x": 1593930000000, - "y": 1, - }, - Object { - "x": 1593930600000, - "y": 0, - }, - Object { - "x": 1593931200000, - "y": 0, - }, - Object { - "x": 1593931800000, - "y": 0, - }, - Object { - "x": 1593932400000, - "y": 0.5, - }, - Object { - "x": 1593933000000, - "y": 0, - }, - Object { - "x": 1593933600000, - "y": 0, - }, - Object { - "x": 1593934200000, - "y": 0, - }, - Object { - "x": 1593934800000, - "y": 0, - }, - Object { - "x": 1593935400000, - "y": 0, - }, - Object { - "x": 1593936000000, - "y": 0, - }, - Object { - "x": 1593936600000, - "y": 3, - }, - Object { - "x": 1593937200000, - "y": 8.5, - }, - Object { - "x": 1593937800000, - "y": 8, - }, - Object { - "x": 1593938400000, - "y": 2.5, - }, - ], - "key": "HTTP 5xx", - }, - Object { - "avg": 0, - "dataPoints": Array [], - "key": "A Custom Bucket (that should be last)", - }, - ], -} -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts deleted file mode 100644 index 75dfae3e7375f..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ /dev/null @@ -1,69 +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 { ESResponse, timeseriesFetcher } from './fetcher'; -import { APMConfig } from '../../../../../server'; -import { ProcessorEvent } from '../../../../../common/processor_event'; - -describe('timeseriesFetcher', () => { - let res: ESResponse; - let clientSpy: jest.Mock; - beforeEach(async () => { - clientSpy = jest.fn().mockResolvedValueOnce('ES response'); - - res = await timeseriesFetcher({ - serviceName: 'myServiceName', - transactionType: 'myTransactionType', - transactionName: undefined, - setup: { - start: 1528113600000, - end: 1528977600000, - apmEventClient: { search: clientSpy } as any, - internalClient: { search: clientSpy } as any, - config: new Proxy( - {}, - { - get: () => 'myIndex', - } - ) as APMConfig, - uiFilters: { - environment: 'test', - }, - esFilter: [ - { - term: { 'service.environment': 'test' }, - }, - ], - indices: { - /* eslint-disable @typescript-eslint/naming-convention */ - 'apm_oss.sourcemapIndices': 'myIndex', - 'apm_oss.errorIndices': 'myIndex', - 'apm_oss.onboardingIndices': 'myIndex', - 'apm_oss.spanIndices': 'myIndex', - 'apm_oss.transactionIndices': 'myIndex', - 'apm_oss.metricsIndices': 'myIndex', - /* eslint-enable @typescript-eslint/naming-convention */ - apmAgentConfigurationIndex: 'myIndex', - apmCustomLinkIndex: 'myIndex', - }, - }, - searchAggregatedTransactions: false, - }); - }); - - it('should call client with correct query', () => { - expect(clientSpy.mock.calls).toMatchSnapshot(); - }); - - it('should restrict results to only transaction documents', () => { - const query = clientSpy.mock.calls[0][0]; - expect(query.apm.events).toEqual([ProcessorEvent.transaction]); - }); - - it('should return correct response', () => { - expect(res).toBe('ES response'); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts deleted file mode 100644 index 6c923290848a1..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getBucketSize } from '../../../helpers/get_bucket_size'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; -import { timeseriesFetcher } from './fetcher'; -import { timeseriesTransformer } from './transform'; - -export async function getApmTimeseriesData(options: { - serviceName: string; - transactionType: string | undefined; - transactionName: string | undefined; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; -}) { - const { start, end } = options.setup; - const { bucketSize } = getBucketSize({ start, end }); - const durationAsMinutes = (end - start) / 1000 / 60; - - const timeseriesResponse = await timeseriesFetcher(options); - return timeseriesTransformer({ - timeseriesResponse, - bucketSize, - durationAsMinutes, - }); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock_responses/timeseries_response.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock_responses/timeseries_response.ts deleted file mode 100644 index 67084f8a42536..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock_responses/timeseries_response.ts +++ /dev/null @@ -1,6748 +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 { ESResponse } from '../fetcher'; - -export const timeseriesResponse = ({ - took: 206, - timed_out: false, - _shards: { - total: 9, - successful: 9, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 10000, - relation: 'gte', - }, - max_score: null, - hits: [], - }, - aggregations: { - transaction_results: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'A Custom Bucket (that should be last)', - doc_count: 0, - timeseries: { buckets: [] }, - }, - { - key: 'HTTP 2xx', - doc_count: 12150, - timeseries: { - buckets: [ - { - key_as_string: '2020-07-04T08:40:00.000Z', - key: 1593852000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T08:50:00.000Z', - key: 1593852600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:00:00.000Z', - key: 1593853200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:10:00.000Z', - key: 1593853800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:20:00.000Z', - key: 1593854400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:30:00.000Z', - key: 1593855000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:40:00.000Z', - key: 1593855600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:50:00.000Z', - key: 1593856200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:00:00.000Z', - key: 1593856800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:10:00.000Z', - key: 1593857400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:20:00.000Z', - key: 1593858000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:30:00.000Z', - key: 1593858600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:40:00.000Z', - key: 1593859200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:50:00.000Z', - key: 1593859800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:00:00.000Z', - key: 1593860400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:10:00.000Z', - key: 1593861000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:20:00.000Z', - key: 1593861600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:30:00.000Z', - key: 1593862200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:40:00.000Z', - key: 1593862800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:50:00.000Z', - key: 1593863400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:00:00.000Z', - key: 1593864000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:10:00.000Z', - key: 1593864600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:20:00.000Z', - key: 1593865200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:30:00.000Z', - key: 1593865800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:40:00.000Z', - key: 1593866400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:50:00.000Z', - key: 1593867000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:00:00.000Z', - key: 1593867600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:10:00.000Z', - key: 1593868200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:20:00.000Z', - key: 1593868800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:30:00.000Z', - key: 1593869400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:40:00.000Z', - key: 1593870000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:50:00.000Z', - key: 1593870600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:00:00.000Z', - key: 1593871200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:10:00.000Z', - key: 1593871800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:20:00.000Z', - key: 1593872400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:30:00.000Z', - key: 1593873000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:40:00.000Z', - key: 1593873600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:50:00.000Z', - key: 1593874200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:00:00.000Z', - key: 1593874800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:10:00.000Z', - key: 1593875400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:20:00.000Z', - key: 1593876000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:30:00.000Z', - key: 1593876600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:40:00.000Z', - key: 1593877200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:50:00.000Z', - key: 1593877800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:00:00.000Z', - key: 1593878400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:10:00.000Z', - key: 1593879000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:20:00.000Z', - key: 1593879600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:30:00.000Z', - key: 1593880200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:40:00.000Z', - key: 1593880800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:50:00.000Z', - key: 1593881400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:00:00.000Z', - key: 1593882000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:10:00.000Z', - key: 1593882600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:20:00.000Z', - key: 1593883200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:30:00.000Z', - key: 1593883800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:40:00.000Z', - key: 1593884400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:50:00.000Z', - key: 1593885000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:00:00.000Z', - key: 1593885600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:10:00.000Z', - key: 1593886200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:20:00.000Z', - key: 1593886800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:30:00.000Z', - key: 1593887400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:40:00.000Z', - key: 1593888000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:50:00.000Z', - key: 1593888600000, - doc_count: 169, - count: { - value: 169, - }, - }, - { - key_as_string: '2020-07-04T19:00:00.000Z', - key: 1593889200000, - doc_count: 444, - count: { - value: 444, - }, - }, - { - key_as_string: '2020-07-04T19:10:00.000Z', - key: 1593889800000, - doc_count: 460, - count: { - value: 460, - }, - }, - { - key_as_string: '2020-07-04T19:20:00.000Z', - key: 1593890400000, - doc_count: 506, - count: { - value: 506, - }, - }, - { - key_as_string: '2020-07-04T19:30:00.000Z', - key: 1593891000000, - doc_count: 479, - count: { - value: 479, - }, - }, - { - key_as_string: '2020-07-04T19:40:00.000Z', - key: 1593891600000, - doc_count: 457, - count: { - value: 457, - }, - }, - { - key_as_string: '2020-07-04T19:50:00.000Z', - key: 1593892200000, - doc_count: 514, - count: { - value: 514, - }, - }, - { - key_as_string: '2020-07-04T20:00:00.000Z', - key: 1593892800000, - doc_count: 482, - count: { - value: 482, - }, - }, - { - key_as_string: '2020-07-04T20:10:00.000Z', - key: 1593893400000, - doc_count: 504, - count: { - value: 504, - }, - }, - { - key_as_string: '2020-07-04T20:20:00.000Z', - key: 1593894000000, - doc_count: 532, - count: { - value: 532, - }, - }, - { - key_as_string: '2020-07-04T20:30:00.000Z', - key: 1593894600000, - doc_count: 458, - count: { - value: 458, - }, - }, - { - key_as_string: '2020-07-04T20:40:00.000Z', - key: 1593895200000, - doc_count: 448, - count: { - value: 448, - }, - }, - { - key_as_string: '2020-07-04T20:50:00.000Z', - key: 1593895800000, - doc_count: 468, - count: { - value: 468, - }, - }, - { - key_as_string: '2020-07-04T21:00:00.000Z', - key: 1593896400000, - doc_count: 526, - count: { - value: 526, - }, - }, - { - key_as_string: '2020-07-04T21:10:00.000Z', - key: 1593897000000, - doc_count: 495, - count: { - value: 495, - }, - }, - { - key_as_string: '2020-07-04T21:20:00.000Z', - key: 1593897600000, - doc_count: 492, - count: { - value: 492, - }, - }, - { - key_as_string: '2020-07-04T21:30:00.000Z', - key: 1593898200000, - doc_count: 487, - count: { - value: 487, - }, - }, - { - key_as_string: '2020-07-04T21:40:00.000Z', - key: 1593898800000, - doc_count: 491, - count: { - value: 491, - }, - }, - { - key_as_string: '2020-07-04T21:50:00.000Z', - key: 1593899400000, - doc_count: 486, - count: { - value: 486, - }, - }, - { - key_as_string: '2020-07-04T22:00:00.000Z', - key: 1593900000000, - doc_count: 458, - count: { - value: 458, - }, - }, - { - key_as_string: '2020-07-04T22:10:00.000Z', - key: 1593900600000, - doc_count: 528, - count: { - value: 528, - }, - }, - { - key_as_string: '2020-07-04T22:20:00.000Z', - key: 1593901200000, - doc_count: 467, - count: { - value: 467, - }, - }, - { - key_as_string: '2020-07-04T22:30:00.000Z', - key: 1593901800000, - doc_count: 179, - count: { - value: 179, - }, - }, - { - key_as_string: '2020-07-04T22:40:00.000Z', - key: 1593902400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T22:50:00.000Z', - key: 1593903000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:00:00.000Z', - key: 1593903600000, - doc_count: 39, - count: { - value: 39, - }, - }, - { - key_as_string: '2020-07-04T23:10:00.000Z', - key: 1593904200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:20:00.000Z', - key: 1593904800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:30:00.000Z', - key: 1593905400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:40:00.000Z', - key: 1593906000000, - doc_count: 36, - count: { - value: 36, - }, - }, - { - key_as_string: '2020-07-04T23:50:00.000Z', - key: 1593906600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:00:00.000Z', - key: 1593907200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:10:00.000Z', - key: 1593907800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:20:00.000Z', - key: 1593908400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:30:00.000Z', - key: 1593909000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:40:00.000Z', - key: 1593909600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:50:00.000Z', - key: 1593910200000, - doc_count: 34, - count: { - value: 34, - }, - }, - { - key_as_string: '2020-07-05T01:00:00.000Z', - key: 1593910800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:10:00.000Z', - key: 1593911400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:20:00.000Z', - key: 1593912000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:30:00.000Z', - key: 1593912600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:40:00.000Z', - key: 1593913200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:50:00.000Z', - key: 1593913800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:00:00.000Z', - key: 1593914400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:10:00.000Z', - key: 1593915000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:20:00.000Z', - key: 1593915600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:30:00.000Z', - key: 1593916200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:40:00.000Z', - key: 1593916800000, - doc_count: 31, - count: { - value: 31, - }, - }, - { - key_as_string: '2020-07-05T02:50:00.000Z', - key: 1593917400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:00:00.000Z', - key: 1593918000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:10:00.000Z', - key: 1593918600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:20:00.000Z', - key: 1593919200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:30:00.000Z', - key: 1593919800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:40:00.000Z', - key: 1593920400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:50:00.000Z', - key: 1593921000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:00:00.000Z', - key: 1593921600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:10:00.000Z', - key: 1593922200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:20:00.000Z', - key: 1593922800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:30:00.000Z', - key: 1593923400000, - doc_count: 49, - count: { - value: 49, - }, - }, - { - key_as_string: '2020-07-05T04:40:00.000Z', - key: 1593924000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:50:00.000Z', - key: 1593924600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:00:00.000Z', - key: 1593925200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:10:00.000Z', - key: 1593925800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:20:00.000Z', - key: 1593926400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:30:00.000Z', - key: 1593927000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:40:00.000Z', - key: 1593927600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:50:00.000Z', - key: 1593928200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:00:00.000Z', - key: 1593928800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:10:00.000Z', - key: 1593929400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:20:00.000Z', - key: 1593930000000, - doc_count: 50, - count: { - value: 50, - }, - }, - { - key_as_string: '2020-07-05T06:30:00.000Z', - key: 1593930600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:40:00.000Z', - key: 1593931200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:50:00.000Z', - key: 1593931800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:00:00.000Z', - key: 1593932400000, - doc_count: 37, - count: { - value: 37, - }, - }, - { - key_as_string: '2020-07-05T07:10:00.000Z', - key: 1593933000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:20:00.000Z', - key: 1593933600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:30:00.000Z', - key: 1593934200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:40:00.000Z', - key: 1593934800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:50:00.000Z', - key: 1593935400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T08:00:00.000Z', - key: 1593936000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T08:10:00.000Z', - key: 1593936600000, - doc_count: 194, - count: { - value: 194, - }, - }, - { - key_as_string: '2020-07-05T08:20:00.000Z', - key: 1593937200000, - doc_count: 385, - count: { - value: 385, - }, - }, - { - key_as_string: '2020-07-05T08:30:00.000Z', - key: 1593937800000, - doc_count: 421, - count: { - value: 421, - }, - }, - { - key_as_string: '2020-07-05T08:40:00.000Z', - key: 1593938400000, - doc_count: 344, - count: { - value: 344, - }, - }, - ], - }, - }, - { - key: 'HTTP 3xx', - doc_count: 3828, - timeseries: { - buckets: [ - { - key_as_string: '2020-07-04T08:40:00.000Z', - key: 1593852000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T08:50:00.000Z', - key: 1593852600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:00:00.000Z', - key: 1593853200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:10:00.000Z', - key: 1593853800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:20:00.000Z', - key: 1593854400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:30:00.000Z', - key: 1593855000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:40:00.000Z', - key: 1593855600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:50:00.000Z', - key: 1593856200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:00:00.000Z', - key: 1593856800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:10:00.000Z', - key: 1593857400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:20:00.000Z', - key: 1593858000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:30:00.000Z', - key: 1593858600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:40:00.000Z', - key: 1593859200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:50:00.000Z', - key: 1593859800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:00:00.000Z', - key: 1593860400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:10:00.000Z', - key: 1593861000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:20:00.000Z', - key: 1593861600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:30:00.000Z', - key: 1593862200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:40:00.000Z', - key: 1593862800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:50:00.000Z', - key: 1593863400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:00:00.000Z', - key: 1593864000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:10:00.000Z', - key: 1593864600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:20:00.000Z', - key: 1593865200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:30:00.000Z', - key: 1593865800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:40:00.000Z', - key: 1593866400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:50:00.000Z', - key: 1593867000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:00:00.000Z', - key: 1593867600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:10:00.000Z', - key: 1593868200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:20:00.000Z', - key: 1593868800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:30:00.000Z', - key: 1593869400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:40:00.000Z', - key: 1593870000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:50:00.000Z', - key: 1593870600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:00:00.000Z', - key: 1593871200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:10:00.000Z', - key: 1593871800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:20:00.000Z', - key: 1593872400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:30:00.000Z', - key: 1593873000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:40:00.000Z', - key: 1593873600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:50:00.000Z', - key: 1593874200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:00:00.000Z', - key: 1593874800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:10:00.000Z', - key: 1593875400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:20:00.000Z', - key: 1593876000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:30:00.000Z', - key: 1593876600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:40:00.000Z', - key: 1593877200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:50:00.000Z', - key: 1593877800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:00:00.000Z', - key: 1593878400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:10:00.000Z', - key: 1593879000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:20:00.000Z', - key: 1593879600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:30:00.000Z', - key: 1593880200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:40:00.000Z', - key: 1593880800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:50:00.000Z', - key: 1593881400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:00:00.000Z', - key: 1593882000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:10:00.000Z', - key: 1593882600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:20:00.000Z', - key: 1593883200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:30:00.000Z', - key: 1593883800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:40:00.000Z', - key: 1593884400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:50:00.000Z', - key: 1593885000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:00:00.000Z', - key: 1593885600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:10:00.000Z', - key: 1593886200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:20:00.000Z', - key: 1593886800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:30:00.000Z', - key: 1593887400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:40:00.000Z', - key: 1593888000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:50:00.000Z', - key: 1593888600000, - doc_count: 62, - count: { - value: 62, - }, - }, - { - key_as_string: '2020-07-04T19:00:00.000Z', - key: 1593889200000, - doc_count: 52, - count: { - value: 52, - }, - }, - { - key_as_string: '2020-07-04T19:10:00.000Z', - key: 1593889800000, - doc_count: 128, - count: { - value: 128, - }, - }, - { - key_as_string: '2020-07-04T19:20:00.000Z', - key: 1593890400000, - doc_count: 143, - count: { - value: 143, - }, - }, - { - key_as_string: '2020-07-04T19:30:00.000Z', - key: 1593891000000, - doc_count: 129, - count: { - value: 129, - }, - }, - { - key_as_string: '2020-07-04T19:40:00.000Z', - key: 1593891600000, - doc_count: 121, - count: { - value: 121, - }, - }, - { - key_as_string: '2020-07-04T19:50:00.000Z', - key: 1593892200000, - doc_count: 292, - count: { - value: 292, - }, - }, - { - key_as_string: '2020-07-04T20:00:00.000Z', - key: 1593892800000, - doc_count: 139, - count: { - value: 139, - }, - }, - { - key_as_string: '2020-07-04T20:10:00.000Z', - key: 1593893400000, - doc_count: 104, - count: { - value: 104, - }, - }, - { - key_as_string: '2020-07-04T20:20:00.000Z', - key: 1593894000000, - doc_count: 198, - count: { - value: 198, - }, - }, - { - key_as_string: '2020-07-04T20:30:00.000Z', - key: 1593894600000, - doc_count: 179, - count: { - value: 179, - }, - }, - { - key_as_string: '2020-07-04T20:40:00.000Z', - key: 1593895200000, - doc_count: 117, - count: { - value: 117, - }, - }, - { - key_as_string: '2020-07-04T20:50:00.000Z', - key: 1593895800000, - doc_count: 183, - count: { - value: 183, - }, - }, - { - key_as_string: '2020-07-04T21:00:00.000Z', - key: 1593896400000, - doc_count: 264, - count: { - value: 264, - }, - }, - { - key_as_string: '2020-07-04T21:10:00.000Z', - key: 1593897000000, - doc_count: 180, - count: { - value: 180, - }, - }, - { - key_as_string: '2020-07-04T21:20:00.000Z', - key: 1593897600000, - doc_count: 160, - count: { - value: 160, - }, - }, - { - key_as_string: '2020-07-04T21:30:00.000Z', - key: 1593898200000, - doc_count: 208, - count: { - value: 208, - }, - }, - { - key_as_string: '2020-07-04T21:40:00.000Z', - key: 1593898800000, - doc_count: 158, - count: { - value: 158, - }, - }, - { - key_as_string: '2020-07-04T21:50:00.000Z', - key: 1593899400000, - doc_count: 176, - count: { - value: 176, - }, - }, - { - key_as_string: '2020-07-04T22:00:00.000Z', - key: 1593900000000, - doc_count: 183, - count: { - value: 183, - }, - }, - { - key_as_string: '2020-07-04T22:10:00.000Z', - key: 1593900600000, - doc_count: 234, - count: { - value: 234, - }, - }, - { - key_as_string: '2020-07-04T22:20:00.000Z', - key: 1593901200000, - doc_count: 125, - count: { - value: 125, - }, - }, - { - key_as_string: '2020-07-04T22:30:00.000Z', - key: 1593901800000, - doc_count: 48, - count: { - value: 48, - }, - }, - { - key_as_string: '2020-07-04T22:40:00.000Z', - key: 1593902400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T22:50:00.000Z', - key: 1593903000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:00:00.000Z', - key: 1593903600000, - doc_count: 9, - count: { - value: 9, - }, - }, - { - key_as_string: '2020-07-04T23:10:00.000Z', - key: 1593904200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:20:00.000Z', - key: 1593904800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:30:00.000Z', - key: 1593905400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:40:00.000Z', - key: 1593906000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:50:00.000Z', - key: 1593906600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:00:00.000Z', - key: 1593907200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:10:00.000Z', - key: 1593907800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:20:00.000Z', - key: 1593908400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:30:00.000Z', - key: 1593909000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:40:00.000Z', - key: 1593909600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:50:00.000Z', - key: 1593910200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:00:00.000Z', - key: 1593910800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:10:00.000Z', - key: 1593911400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:20:00.000Z', - key: 1593912000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:30:00.000Z', - key: 1593912600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:40:00.000Z', - key: 1593913200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:50:00.000Z', - key: 1593913800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:00:00.000Z', - key: 1593914400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:10:00.000Z', - key: 1593915000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:20:00.000Z', - key: 1593915600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:30:00.000Z', - key: 1593916200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:40:00.000Z', - key: 1593916800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:50:00.000Z', - key: 1593917400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:00:00.000Z', - key: 1593918000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:10:00.000Z', - key: 1593918600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:20:00.000Z', - key: 1593919200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:30:00.000Z', - key: 1593919800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:40:00.000Z', - key: 1593920400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:50:00.000Z', - key: 1593921000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:00:00.000Z', - key: 1593921600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:10:00.000Z', - key: 1593922200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:20:00.000Z', - key: 1593922800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:30:00.000Z', - key: 1593923400000, - doc_count: 11, - count: { - value: 11, - }, - }, - { - key_as_string: '2020-07-05T04:40:00.000Z', - key: 1593924000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:50:00.000Z', - key: 1593924600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:00:00.000Z', - key: 1593925200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:10:00.000Z', - key: 1593925800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:20:00.000Z', - key: 1593926400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:30:00.000Z', - key: 1593927000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:40:00.000Z', - key: 1593927600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:50:00.000Z', - key: 1593928200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:00:00.000Z', - key: 1593928800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:10:00.000Z', - key: 1593929400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:20:00.000Z', - key: 1593930000000, - doc_count: 28, - count: { - value: 28, - }, - }, - { - key_as_string: '2020-07-05T06:30:00.000Z', - key: 1593930600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:40:00.000Z', - key: 1593931200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:50:00.000Z', - key: 1593931800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:00:00.000Z', - key: 1593932400000, - doc_count: 2, - count: { - value: 2, - }, - }, - { - key_as_string: '2020-07-05T07:10:00.000Z', - key: 1593933000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:20:00.000Z', - key: 1593933600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:30:00.000Z', - key: 1593934200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:40:00.000Z', - key: 1593934800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:50:00.000Z', - key: 1593935400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T08:00:00.000Z', - key: 1593936000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T08:10:00.000Z', - key: 1593936600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T08:20:00.000Z', - key: 1593937200000, - doc_count: 63, - count: { - value: 63, - }, - }, - { - key_as_string: '2020-07-05T08:30:00.000Z', - key: 1593937800000, - doc_count: 50, - count: { - value: 50, - }, - }, - { - key_as_string: '2020-07-05T08:40:00.000Z', - key: 1593938400000, - doc_count: 82, - count: { - value: 82, - }, - }, - ], - }, - }, - { - key: 'HTTP 4xx', - doc_count: 683, - timeseries: { - buckets: [ - { - key_as_string: '2020-07-04T08:40:00.000Z', - key: 1593852000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T08:50:00.000Z', - key: 1593852600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:00:00.000Z', - key: 1593853200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:10:00.000Z', - key: 1593853800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:20:00.000Z', - key: 1593854400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:30:00.000Z', - key: 1593855000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:40:00.000Z', - key: 1593855600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:50:00.000Z', - key: 1593856200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:00:00.000Z', - key: 1593856800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:10:00.000Z', - key: 1593857400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:20:00.000Z', - key: 1593858000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:30:00.000Z', - key: 1593858600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:40:00.000Z', - key: 1593859200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:50:00.000Z', - key: 1593859800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:00:00.000Z', - key: 1593860400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:10:00.000Z', - key: 1593861000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:20:00.000Z', - key: 1593861600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:30:00.000Z', - key: 1593862200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:40:00.000Z', - key: 1593862800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:50:00.000Z', - key: 1593863400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:00:00.000Z', - key: 1593864000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:10:00.000Z', - key: 1593864600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:20:00.000Z', - key: 1593865200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:30:00.000Z', - key: 1593865800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:40:00.000Z', - key: 1593866400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:50:00.000Z', - key: 1593867000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:00:00.000Z', - key: 1593867600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:10:00.000Z', - key: 1593868200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:20:00.000Z', - key: 1593868800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:30:00.000Z', - key: 1593869400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:40:00.000Z', - key: 1593870000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:50:00.000Z', - key: 1593870600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:00:00.000Z', - key: 1593871200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:10:00.000Z', - key: 1593871800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:20:00.000Z', - key: 1593872400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:30:00.000Z', - key: 1593873000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:40:00.000Z', - key: 1593873600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:50:00.000Z', - key: 1593874200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:00:00.000Z', - key: 1593874800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:10:00.000Z', - key: 1593875400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:20:00.000Z', - key: 1593876000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:30:00.000Z', - key: 1593876600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:40:00.000Z', - key: 1593877200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:50:00.000Z', - key: 1593877800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:00:00.000Z', - key: 1593878400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:10:00.000Z', - key: 1593879000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:20:00.000Z', - key: 1593879600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:30:00.000Z', - key: 1593880200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:40:00.000Z', - key: 1593880800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:50:00.000Z', - key: 1593881400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:00:00.000Z', - key: 1593882000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:10:00.000Z', - key: 1593882600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:20:00.000Z', - key: 1593883200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:30:00.000Z', - key: 1593883800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:40:00.000Z', - key: 1593884400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:50:00.000Z', - key: 1593885000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:00:00.000Z', - key: 1593885600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:10:00.000Z', - key: 1593886200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:20:00.000Z', - key: 1593886800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:30:00.000Z', - key: 1593887400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:40:00.000Z', - key: 1593888000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:50:00.000Z', - key: 1593888600000, - doc_count: 11, - count: { - value: 11, - }, - }, - { - key_as_string: '2020-07-04T19:00:00.000Z', - key: 1593889200000, - doc_count: 31, - count: { - value: 31, - }, - }, - { - key_as_string: '2020-07-04T19:10:00.000Z', - key: 1593889800000, - doc_count: 19, - count: { - value: 19, - }, - }, - { - key_as_string: '2020-07-04T19:20:00.000Z', - key: 1593890400000, - doc_count: 23, - count: { - value: 23, - }, - }, - { - key_as_string: '2020-07-04T19:30:00.000Z', - key: 1593891000000, - doc_count: 26, - count: { - value: 26, - }, - }, - { - key_as_string: '2020-07-04T19:40:00.000Z', - key: 1593891600000, - doc_count: 27, - count: { - value: 27, - }, - }, - { - key_as_string: '2020-07-04T19:50:00.000Z', - key: 1593892200000, - doc_count: 27, - count: { - value: 27, - }, - }, - { - key_as_string: '2020-07-04T20:00:00.000Z', - key: 1593892800000, - doc_count: 30, - count: { - value: 30, - }, - }, - { - key_as_string: '2020-07-04T20:10:00.000Z', - key: 1593893400000, - doc_count: 28, - count: { - value: 28, - }, - }, - { - key_as_string: '2020-07-04T20:20:00.000Z', - key: 1593894000000, - doc_count: 33, - count: { - value: 33, - }, - }, - { - key_as_string: '2020-07-04T20:30:00.000Z', - key: 1593894600000, - doc_count: 23, - count: { - value: 23, - }, - }, - { - key_as_string: '2020-07-04T20:40:00.000Z', - key: 1593895200000, - doc_count: 35, - count: { - value: 35, - }, - }, - { - key_as_string: '2020-07-04T20:50:00.000Z', - key: 1593895800000, - doc_count: 26, - count: { - value: 26, - }, - }, - { - key_as_string: '2020-07-04T21:00:00.000Z', - key: 1593896400000, - doc_count: 35, - count: { - value: 35, - }, - }, - { - key_as_string: '2020-07-04T21:10:00.000Z', - key: 1593897000000, - doc_count: 25, - count: { - value: 25, - }, - }, - { - key_as_string: '2020-07-04T21:20:00.000Z', - key: 1593897600000, - doc_count: 26, - count: { - value: 26, - }, - }, - { - key_as_string: '2020-07-04T21:30:00.000Z', - key: 1593898200000, - doc_count: 25, - count: { - value: 25, - }, - }, - { - key_as_string: '2020-07-04T21:40:00.000Z', - key: 1593898800000, - doc_count: 17, - count: { - value: 17, - }, - }, - { - key_as_string: '2020-07-04T21:50:00.000Z', - key: 1593899400000, - doc_count: 19, - count: { - value: 19, - }, - }, - { - key_as_string: '2020-07-04T22:00:00.000Z', - key: 1593900000000, - doc_count: 28, - count: { - value: 28, - }, - }, - { - key_as_string: '2020-07-04T22:10:00.000Z', - key: 1593900600000, - doc_count: 24, - count: { - value: 24, - }, - }, - { - key_as_string: '2020-07-04T22:20:00.000Z', - key: 1593901200000, - doc_count: 30, - count: { - value: 30, - }, - }, - { - key_as_string: '2020-07-04T22:30:00.000Z', - key: 1593901800000, - doc_count: 6, - count: { - value: 6, - }, - }, - { - key_as_string: '2020-07-04T22:40:00.000Z', - key: 1593902400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T22:50:00.000Z', - key: 1593903000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:00:00.000Z', - key: 1593903600000, - doc_count: 2, - count: { - value: 2, - }, - }, - { - key_as_string: '2020-07-04T23:10:00.000Z', - key: 1593904200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:20:00.000Z', - key: 1593904800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:30:00.000Z', - key: 1593905400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:40:00.000Z', - key: 1593906000000, - doc_count: 4, - count: { - value: 4, - }, - }, - { - key_as_string: '2020-07-04T23:50:00.000Z', - key: 1593906600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:00:00.000Z', - key: 1593907200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:10:00.000Z', - key: 1593907800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:20:00.000Z', - key: 1593908400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:30:00.000Z', - key: 1593909000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:40:00.000Z', - key: 1593909600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:50:00.000Z', - key: 1593910200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:00:00.000Z', - key: 1593910800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:10:00.000Z', - key: 1593911400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:20:00.000Z', - key: 1593912000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:30:00.000Z', - key: 1593912600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:40:00.000Z', - key: 1593913200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:50:00.000Z', - key: 1593913800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:00:00.000Z', - key: 1593914400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:10:00.000Z', - key: 1593915000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:20:00.000Z', - key: 1593915600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:30:00.000Z', - key: 1593916200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:40:00.000Z', - key: 1593916800000, - doc_count: 3, - count: { - value: 3, - }, - }, - { - key_as_string: '2020-07-05T02:50:00.000Z', - key: 1593917400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:00:00.000Z', - key: 1593918000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:10:00.000Z', - key: 1593918600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:20:00.000Z', - key: 1593919200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:30:00.000Z', - key: 1593919800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:40:00.000Z', - key: 1593920400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:50:00.000Z', - key: 1593921000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:00:00.000Z', - key: 1593921600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:10:00.000Z', - key: 1593922200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:20:00.000Z', - key: 1593922800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:30:00.000Z', - key: 1593923400000, - doc_count: 2, - count: { - value: 2, - }, - }, - { - key_as_string: '2020-07-05T04:40:00.000Z', - key: 1593924000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:50:00.000Z', - key: 1593924600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:00:00.000Z', - key: 1593925200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:10:00.000Z', - key: 1593925800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:20:00.000Z', - key: 1593926400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:30:00.000Z', - key: 1593927000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:40:00.000Z', - key: 1593927600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:50:00.000Z', - key: 1593928200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:00:00.000Z', - key: 1593928800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:10:00.000Z', - key: 1593929400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:20:00.000Z', - key: 1593930000000, - doc_count: 3, - count: { - value: 3, - }, - }, - { - key_as_string: '2020-07-05T06:30:00.000Z', - key: 1593930600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:40:00.000Z', - key: 1593931200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:50:00.000Z', - key: 1593931800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:00:00.000Z', - key: 1593932400000, - doc_count: 2, - count: { - value: 2, - }, - }, - { - key_as_string: '2020-07-05T07:10:00.000Z', - key: 1593933000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:20:00.000Z', - key: 1593933600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:30:00.000Z', - key: 1593934200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:40:00.000Z', - key: 1593934800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:50:00.000Z', - key: 1593935400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T08:00:00.000Z', - key: 1593936000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T08:10:00.000Z', - key: 1593936600000, - doc_count: 15, - count: { - value: 15, - }, - }, - { - key_as_string: '2020-07-05T08:20:00.000Z', - key: 1593937200000, - doc_count: 29, - count: { - value: 29, - }, - }, - { - key_as_string: '2020-07-05T08:30:00.000Z', - key: 1593937800000, - doc_count: 31, - count: { - value: 31, - }, - }, - { - key_as_string: '2020-07-05T08:40:00.000Z', - key: 1593938400000, - doc_count: 18, - count: { - value: 18, - }, - }, - ], - }, - }, - { - key: 'HTTP 5xx', - doc_count: 378, - timeseries: { - buckets: [ - { - key_as_string: '2020-07-04T08:40:00.000Z', - key: 1593852000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T08:50:00.000Z', - key: 1593852600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:00:00.000Z', - key: 1593853200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:10:00.000Z', - key: 1593853800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:20:00.000Z', - key: 1593854400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:30:00.000Z', - key: 1593855000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:40:00.000Z', - key: 1593855600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T09:50:00.000Z', - key: 1593856200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:00:00.000Z', - key: 1593856800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:10:00.000Z', - key: 1593857400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:20:00.000Z', - key: 1593858000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:30:00.000Z', - key: 1593858600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:40:00.000Z', - key: 1593859200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T10:50:00.000Z', - key: 1593859800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:00:00.000Z', - key: 1593860400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:10:00.000Z', - key: 1593861000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:20:00.000Z', - key: 1593861600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:30:00.000Z', - key: 1593862200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:40:00.000Z', - key: 1593862800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T11:50:00.000Z', - key: 1593863400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:00:00.000Z', - key: 1593864000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:10:00.000Z', - key: 1593864600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:20:00.000Z', - key: 1593865200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:30:00.000Z', - key: 1593865800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:40:00.000Z', - key: 1593866400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T12:50:00.000Z', - key: 1593867000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:00:00.000Z', - key: 1593867600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:10:00.000Z', - key: 1593868200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:20:00.000Z', - key: 1593868800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:30:00.000Z', - key: 1593869400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:40:00.000Z', - key: 1593870000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T13:50:00.000Z', - key: 1593870600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:00:00.000Z', - key: 1593871200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:10:00.000Z', - key: 1593871800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:20:00.000Z', - key: 1593872400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:30:00.000Z', - key: 1593873000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:40:00.000Z', - key: 1593873600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T14:50:00.000Z', - key: 1593874200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:00:00.000Z', - key: 1593874800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:10:00.000Z', - key: 1593875400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:20:00.000Z', - key: 1593876000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:30:00.000Z', - key: 1593876600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:40:00.000Z', - key: 1593877200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T15:50:00.000Z', - key: 1593877800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:00:00.000Z', - key: 1593878400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:10:00.000Z', - key: 1593879000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:20:00.000Z', - key: 1593879600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:30:00.000Z', - key: 1593880200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:40:00.000Z', - key: 1593880800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T16:50:00.000Z', - key: 1593881400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:00:00.000Z', - key: 1593882000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:10:00.000Z', - key: 1593882600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:20:00.000Z', - key: 1593883200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:30:00.000Z', - key: 1593883800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:40:00.000Z', - key: 1593884400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T17:50:00.000Z', - key: 1593885000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:00:00.000Z', - key: 1593885600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:10:00.000Z', - key: 1593886200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:20:00.000Z', - key: 1593886800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:30:00.000Z', - key: 1593887400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:40:00.000Z', - key: 1593888000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T18:50:00.000Z', - key: 1593888600000, - doc_count: 5, - count: { - value: 5, - }, - }, - { - key_as_string: '2020-07-04T19:00:00.000Z', - key: 1593889200000, - doc_count: 15, - count: { - value: 15, - }, - }, - { - key_as_string: '2020-07-04T19:10:00.000Z', - key: 1593889800000, - doc_count: 12, - count: { - value: 12, - }, - }, - { - key_as_string: '2020-07-04T19:20:00.000Z', - key: 1593890400000, - doc_count: 16, - count: { - value: 16, - }, - }, - { - key_as_string: '2020-07-04T19:30:00.000Z', - key: 1593891000000, - doc_count: 12, - count: { - value: 12, - }, - }, - { - key_as_string: '2020-07-04T19:40:00.000Z', - key: 1593891600000, - doc_count: 16, - count: { - value: 16, - }, - }, - { - key_as_string: '2020-07-04T19:50:00.000Z', - key: 1593892200000, - doc_count: 23, - count: { - value: 23, - }, - }, - { - key_as_string: '2020-07-04T20:00:00.000Z', - key: 1593892800000, - doc_count: 10, - count: { - value: 10, - }, - }, - { - key_as_string: '2020-07-04T20:10:00.000Z', - key: 1593893400000, - doc_count: 10, - count: { - value: 10, - }, - }, - { - key_as_string: '2020-07-04T20:20:00.000Z', - key: 1593894000000, - doc_count: 18, - count: { - value: 18, - }, - }, - { - key_as_string: '2020-07-04T20:30:00.000Z', - key: 1593894600000, - doc_count: 10, - count: { - value: 10, - }, - }, - { - key_as_string: '2020-07-04T20:40:00.000Z', - key: 1593895200000, - doc_count: 17, - count: { - value: 17, - }, - }, - { - key_as_string: '2020-07-04T20:50:00.000Z', - key: 1593895800000, - doc_count: 13, - count: { - value: 13, - }, - }, - { - key_as_string: '2020-07-04T21:00:00.000Z', - key: 1593896400000, - doc_count: 18, - count: { - value: 18, - }, - }, - { - key_as_string: '2020-07-04T21:10:00.000Z', - key: 1593897000000, - doc_count: 17, - count: { - value: 17, - }, - }, - { - key_as_string: '2020-07-04T21:20:00.000Z', - key: 1593897600000, - doc_count: 17, - count: { - value: 17, - }, - }, - { - key_as_string: '2020-07-04T21:30:00.000Z', - key: 1593898200000, - doc_count: 11, - count: { - value: 11, - }, - }, - { - key_as_string: '2020-07-04T21:40:00.000Z', - key: 1593898800000, - doc_count: 10, - count: { - value: 10, - }, - }, - { - key_as_string: '2020-07-04T21:50:00.000Z', - key: 1593899400000, - doc_count: 18, - count: { - value: 18, - }, - }, - { - key_as_string: '2020-07-04T22:00:00.000Z', - key: 1593900000000, - doc_count: 16, - count: { - value: 16, - }, - }, - { - key_as_string: '2020-07-04T22:10:00.000Z', - key: 1593900600000, - doc_count: 12, - count: { - value: 12, - }, - }, - { - key_as_string: '2020-07-04T22:20:00.000Z', - key: 1593901200000, - doc_count: 18, - count: { - value: 18, - }, - }, - { - key_as_string: '2020-07-04T22:30:00.000Z', - key: 1593901800000, - doc_count: 8, - count: { - value: 8, - }, - }, - { - key_as_string: '2020-07-04T22:40:00.000Z', - key: 1593902400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T22:50:00.000Z', - key: 1593903000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:00:00.000Z', - key: 1593903600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:10:00.000Z', - key: 1593904200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:20:00.000Z', - key: 1593904800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:30:00.000Z', - key: 1593905400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-04T23:40:00.000Z', - key: 1593906000000, - doc_count: 1, - count: { - value: 1, - }, - }, - { - key_as_string: '2020-07-04T23:50:00.000Z', - key: 1593906600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:00:00.000Z', - key: 1593907200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:10:00.000Z', - key: 1593907800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:20:00.000Z', - key: 1593908400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:30:00.000Z', - key: 1593909000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:40:00.000Z', - key: 1593909600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T00:50:00.000Z', - key: 1593910200000, - doc_count: 3, - count: { - value: 3, - }, - }, - { - key_as_string: '2020-07-05T01:00:00.000Z', - key: 1593910800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:10:00.000Z', - key: 1593911400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:20:00.000Z', - key: 1593912000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:30:00.000Z', - key: 1593912600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:40:00.000Z', - key: 1593913200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T01:50:00.000Z', - key: 1593913800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:00:00.000Z', - key: 1593914400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:10:00.000Z', - key: 1593915000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:20:00.000Z', - key: 1593915600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:30:00.000Z', - key: 1593916200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T02:40:00.000Z', - key: 1593916800000, - doc_count: 3, - count: { - value: 3, - }, - }, - { - key_as_string: '2020-07-05T02:50:00.000Z', - key: 1593917400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:00:00.000Z', - key: 1593918000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:10:00.000Z', - key: 1593918600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:20:00.000Z', - key: 1593919200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:30:00.000Z', - key: 1593919800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:40:00.000Z', - key: 1593920400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T03:50:00.000Z', - key: 1593921000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:00:00.000Z', - key: 1593921600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:10:00.000Z', - key: 1593922200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:20:00.000Z', - key: 1593922800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:30:00.000Z', - key: 1593923400000, - doc_count: 2, - count: { - value: 2, - }, - }, - { - key_as_string: '2020-07-05T04:40:00.000Z', - key: 1593924000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T04:50:00.000Z', - key: 1593924600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:00:00.000Z', - key: 1593925200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:10:00.000Z', - key: 1593925800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:20:00.000Z', - key: 1593926400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:30:00.000Z', - key: 1593927000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:40:00.000Z', - key: 1593927600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T05:50:00.000Z', - key: 1593928200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:00:00.000Z', - key: 1593928800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:10:00.000Z', - key: 1593929400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:20:00.000Z', - key: 1593930000000, - doc_count: 2, - count: { - value: 2, - }, - }, - { - key_as_string: '2020-07-05T06:30:00.000Z', - key: 1593930600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:40:00.000Z', - key: 1593931200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T06:50:00.000Z', - key: 1593931800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:00:00.000Z', - key: 1593932400000, - doc_count: 1, - count: { - value: 1, - }, - }, - { - key_as_string: '2020-07-05T07:10:00.000Z', - key: 1593933000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:20:00.000Z', - key: 1593933600000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:30:00.000Z', - key: 1593934200000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:40:00.000Z', - key: 1593934800000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T07:50:00.000Z', - key: 1593935400000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T08:00:00.000Z', - key: 1593936000000, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '2020-07-05T08:10:00.000Z', - key: 1593936600000, - doc_count: 6, - count: { - value: 6, - }, - }, - { - key_as_string: '2020-07-05T08:20:00.000Z', - key: 1593937200000, - doc_count: 17, - count: { - value: 17, - }, - }, - { - key_as_string: '2020-07-05T08:30:00.000Z', - key: 1593937800000, - doc_count: 16, - count: { - value: 16, - }, - }, - { - key_as_string: '2020-07-05T08:40:00.000Z', - key: 1593938400000, - doc_count: 5, - count: { - value: 5, - }, - }, - ], - }, - }, - ], - }, - response_times: { - buckets: [ - { - key_as_string: '2020-07-04T08:40:00.000Z', - key: 1593852000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T08:50:00.000Z', - key: 1593852600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T09:00:00.000Z', - key: 1593853200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T09:10:00.000Z', - key: 1593853800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T09:20:00.000Z', - key: 1593854400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T09:30:00.000Z', - key: 1593855000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T09:40:00.000Z', - key: 1593855600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T09:50:00.000Z', - key: 1593856200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T10:00:00.000Z', - key: 1593856800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T10:10:00.000Z', - key: 1593857400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T10:20:00.000Z', - key: 1593858000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T10:30:00.000Z', - key: 1593858600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T10:40:00.000Z', - key: 1593859200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T10:50:00.000Z', - key: 1593859800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T11:00:00.000Z', - key: 1593860400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T11:10:00.000Z', - key: 1593861000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T11:20:00.000Z', - key: 1593861600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T11:30:00.000Z', - key: 1593862200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T11:40:00.000Z', - key: 1593862800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T11:50:00.000Z', - key: 1593863400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T12:00:00.000Z', - key: 1593864000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T12:10:00.000Z', - key: 1593864600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T12:20:00.000Z', - key: 1593865200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T12:30:00.000Z', - key: 1593865800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T12:40:00.000Z', - key: 1593866400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T12:50:00.000Z', - key: 1593867000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T13:00:00.000Z', - key: 1593867600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T13:10:00.000Z', - key: 1593868200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T13:20:00.000Z', - key: 1593868800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T13:30:00.000Z', - key: 1593869400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T13:40:00.000Z', - key: 1593870000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T13:50:00.000Z', - key: 1593870600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T14:00:00.000Z', - key: 1593871200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T14:10:00.000Z', - key: 1593871800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T14:20:00.000Z', - key: 1593872400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T14:30:00.000Z', - key: 1593873000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T14:40:00.000Z', - key: 1593873600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T14:50:00.000Z', - key: 1593874200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T15:00:00.000Z', - key: 1593874800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T15:10:00.000Z', - key: 1593875400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T15:20:00.000Z', - key: 1593876000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T15:30:00.000Z', - key: 1593876600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T15:40:00.000Z', - key: 1593877200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T15:50:00.000Z', - key: 1593877800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T16:00:00.000Z', - key: 1593878400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T16:10:00.000Z', - key: 1593879000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T16:20:00.000Z', - key: 1593879600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T16:30:00.000Z', - key: 1593880200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T16:40:00.000Z', - key: 1593880800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T16:50:00.000Z', - key: 1593881400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T17:00:00.000Z', - key: 1593882000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T17:10:00.000Z', - key: 1593882600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T17:20:00.000Z', - key: 1593883200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T17:30:00.000Z', - key: 1593883800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T17:40:00.000Z', - key: 1593884400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T17:50:00.000Z', - key: 1593885000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T18:00:00.000Z', - key: 1593885600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T18:10:00.000Z', - key: 1593886200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T18:20:00.000Z', - key: 1593886800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T18:30:00.000Z', - key: 1593887400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T18:40:00.000Z', - key: 1593888000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T18:50:00.000Z', - key: 1593888600000, - doc_count: 247, - pct: { - values: { - '95.0': 114680.0, - '99.0': 827384.0, - }, - }, - avg: { - value: 43364.46153846154, - }, - }, - { - key_as_string: '2020-07-04T19:00:00.000Z', - key: 1593889200000, - doc_count: 542, - pct: { - values: { - '95.0': 659448.0, - '99.0': 2326520.0, - }, - }, - avg: { - value: 147903.58671586716, - }, - }, - { - key_as_string: '2020-07-04T19:10:00.000Z', - key: 1593889800000, - doc_count: 619, - pct: { - values: { - '95.0': 122360.0, - '99.0': 1130488.0, - }, - }, - avg: { - value: 57370.52342487884, - }, - }, - { - key_as_string: '2020-07-04T19:20:00.000Z', - key: 1593890400000, - doc_count: 688, - pct: { - values: { - '95.0': 121336.0, - '99.0': 1032184.0, - }, - }, - avg: { - value: 59687.82558139535, - }, - }, - { - key_as_string: '2020-07-04T19:30:00.000Z', - key: 1593891000000, - doc_count: 646, - pct: { - values: { - '95.0': 120828.0, - '99.0': 770044.0, - }, - }, - avg: { - value: 51810.68111455108, - }, - }, - { - key_as_string: '2020-07-04T19:40:00.000Z', - key: 1593891600000, - doc_count: 621, - pct: { - values: { - '95.0': 139256.0, - '99.0': 651256.0, - }, - }, - avg: { - value: 51736.59420289855, - }, - }, - { - key_as_string: '2020-07-04T19:50:00.000Z', - key: 1593892200000, - doc_count: 856, - pct: { - values: { - '95.0': 76792.0, - '99.0': 667640.0, - }, - }, - avg: { - value: 37241.293224299065, - }, - }, - { - key_as_string: '2020-07-04T20:00:00.000Z', - key: 1593892800000, - doc_count: 661, - pct: { - values: { - '95.0': 129528.0, - '99.0': 708600.0, - }, - }, - avg: { - value: 49444.90771558245, - }, - }, - { - key_as_string: '2020-07-04T20:10:00.000Z', - key: 1593893400000, - doc_count: 646, - pct: { - values: { - '95.0': 378872.0, - '99.0': 815096.0, - }, - }, - avg: { - value: 56807.80495356037, - }, - }, - { - key_as_string: '2020-07-04T20:20:00.000Z', - key: 1593894000000, - doc_count: 781, - pct: { - values: { - '95.0': 97272.0, - '99.0': 688120.0, - }, - }, - avg: { - value: 43238.74519846351, - }, - }, - { - key_as_string: '2020-07-04T20:30:00.000Z', - key: 1593894600000, - doc_count: 670, - pct: { - values: { - '95.0': 102904.0, - '99.0': 978936.0, - }, - }, - avg: { - value: 51754.80149253731, - }, - }, - { - key_as_string: '2020-07-04T20:40:00.000Z', - key: 1593895200000, - doc_count: 617, - pct: { - values: { - '95.0': 100856.0, - '99.0': 839672.0, - }, - }, - avg: { - value: 47166.5964343598, - }, - }, - { - key_as_string: '2020-07-04T20:50:00.000Z', - key: 1593895800000, - doc_count: 690, - pct: { - values: { - '95.0': 97784.0, - '99.0': 757752.0, - }, - }, - avg: { - value: 41854.688405797104, - }, - }, - { - key_as_string: '2020-07-04T21:00:00.000Z', - key: 1593896400000, - doc_count: 843, - pct: { - values: { - '95.0': 72700.0, - '99.0': 577532.0, - }, - }, - avg: { - value: 30464.317912218266, - }, - }, - { - key_as_string: '2020-07-04T21:10:00.000Z', - key: 1593897000000, - doc_count: 717, - pct: { - values: { - '95.0': 98296.0, - '99.0': 618488.0, - }, - }, - avg: { - value: 41558.531380753135, - }, - }, - { - key_as_string: '2020-07-04T21:20:00.000Z', - key: 1593897600000, - doc_count: 695, - pct: { - values: { - '95.0': 112120.0, - '99.0': 565240.0, - }, - }, - avg: { - value: 41159.68345323741, - }, - }, - { - key_as_string: '2020-07-04T21:30:00.000Z', - key: 1593898200000, - doc_count: 731, - pct: { - values: { - '95.0': 91640.0, - '99.0': 618488.0, - }, - }, - avg: { - value: 34211.03967168263, - }, - }, - { - key_as_string: '2020-07-04T21:40:00.000Z', - key: 1593898800000, - doc_count: 676, - pct: { - values: { - '95.0': 83448.0, - '99.0': 655352.0, - }, - }, - avg: { - value: 41322.30621301775, - }, - }, - { - key_as_string: '2020-07-04T21:50:00.000Z', - key: 1593899400000, - doc_count: 699, - pct: { - values: { - '95.0': 84476.0, - '99.0': 843772.0, - }, - }, - avg: { - value: 42301.523605150214, - }, - }, - { - key_as_string: '2020-07-04T22:00:00.000Z', - key: 1593900000000, - doc_count: 685, - pct: { - values: { - '95.0': 117756.0, - '99.0': 831484.0, - }, - }, - avg: { - value: 59615.69343065693, - }, - }, - { - key_as_string: '2020-07-04T22:10:00.000Z', - key: 1593900600000, - doc_count: 798, - pct: { - values: { - '95.0': 66556.0, - '99.0': 430076.0, - }, - }, - avg: { - value: 29567.520050125313, - }, - }, - { - key_as_string: '2020-07-04T22:20:00.000Z', - key: 1593901200000, - doc_count: 640, - pct: { - values: { - '95.0': 130552.0, - '99.0': 864248.0, - }, - }, - avg: { - value: 56104.7484375, - }, - }, - { - key_as_string: '2020-07-04T22:30:00.000Z', - key: 1593901800000, - doc_count: 241, - pct: { - values: { - '95.0': 111608.0, - '99.0': 655352.0, - }, - }, - avg: { - value: 40900.70954356847, - }, - }, - { - key_as_string: '2020-07-04T22:40:00.000Z', - key: 1593902400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T22:50:00.000Z', - key: 1593903000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T23:00:00.000Z', - key: 1593903600000, - doc_count: 50, - pct: { - values: { - '95.0': 276448.0, - '99.0': 2883552.0, - }, - }, - avg: { - value: 141618.04, - }, - }, - { - key_as_string: '2020-07-04T23:10:00.000Z', - key: 1593904200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T23:20:00.000Z', - key: 1593904800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T23:30:00.000Z', - key: 1593905400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-04T23:40:00.000Z', - key: 1593906000000, - doc_count: 41, - pct: { - values: { - '95.0': 1028088.0, - '99.0': 6094840.0, - }, - }, - avg: { - value: 380742.48780487804, - }, - }, - { - key_as_string: '2020-07-04T23:50:00.000Z', - key: 1593906600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T00:00:00.000Z', - key: 1593907200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T00:10:00.000Z', - key: 1593907800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T00:20:00.000Z', - key: 1593908400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T00:30:00.000Z', - key: 1593909000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T00:40:00.000Z', - key: 1593909600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T00:50:00.000Z', - key: 1593910200000, - doc_count: 37, - pct: { - values: { - '95.0': 352128.0, - '99.0': 446336.0, - }, - }, - avg: { - value: 122524.7027027027, - }, - }, - { - key_as_string: '2020-07-05T01:00:00.000Z', - key: 1593910800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T01:10:00.000Z', - key: 1593911400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T01:20:00.000Z', - key: 1593912000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T01:30:00.000Z', - key: 1593912600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T01:40:00.000Z', - key: 1593913200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T01:50:00.000Z', - key: 1593913800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T02:00:00.000Z', - key: 1593914400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T02:10:00.000Z', - key: 1593915000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T02:20:00.000Z', - key: 1593915600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T02:30:00.000Z', - key: 1593916200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T02:40:00.000Z', - key: 1593916800000, - doc_count: 37, - pct: { - values: { - '95.0': 348144.0, - '99.0': 3293168.0, - }, - }, - avg: { - value: 160060.1081081081, - }, - }, - { - key_as_string: '2020-07-05T02:50:00.000Z', - key: 1593917400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T03:00:00.000Z', - key: 1593918000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T03:10:00.000Z', - key: 1593918600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T03:20:00.000Z', - key: 1593919200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T03:30:00.000Z', - key: 1593919800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T03:40:00.000Z', - key: 1593920400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T03:50:00.000Z', - key: 1593921000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T04:00:00.000Z', - key: 1593921600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T04:10:00.000Z', - key: 1593922200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T04:20:00.000Z', - key: 1593922800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T04:30:00.000Z', - key: 1593923400000, - doc_count: 64, - pct: { - values: { - '95.0': 270328.0, - '99.0': 299000.0, - }, - }, - avg: { - value: 70357.234375, - }, - }, - { - key_as_string: '2020-07-05T04:40:00.000Z', - key: 1593924000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T04:50:00.000Z', - key: 1593924600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T05:00:00.000Z', - key: 1593925200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T05:10:00.000Z', - key: 1593925800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T05:20:00.000Z', - key: 1593926400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T05:30:00.000Z', - key: 1593927000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T05:40:00.000Z', - key: 1593927600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T05:50:00.000Z', - key: 1593928200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T06:00:00.000Z', - key: 1593928800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T06:10:00.000Z', - key: 1593929400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T06:20:00.000Z', - key: 1593930000000, - doc_count: 83, - pct: { - values: { - '95.0': 1687544.0, - '99.0': 5046264.0, - }, - }, - avg: { - value: 269745.9036144578, - }, - }, - { - key_as_string: '2020-07-05T06:30:00.000Z', - key: 1593930600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T06:40:00.000Z', - key: 1593931200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T06:50:00.000Z', - key: 1593931800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T07:00:00.000Z', - key: 1593932400000, - doc_count: 42, - pct: { - values: { - '95.0': 798656.0, - '99.0': 4292544.0, - }, - }, - avg: { - value: 313349.95238095237, - }, - }, - { - key_as_string: '2020-07-05T07:10:00.000Z', - key: 1593933000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T07:20:00.000Z', - key: 1593933600000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T07:30:00.000Z', - key: 1593934200000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T07:40:00.000Z', - key: 1593934800000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T07:50:00.000Z', - key: 1593935400000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T08:00:00.000Z', - key: 1593936000000, - doc_count: 0, - pct: { - values: { - '95.0': null, - '99.0': null, - }, - }, - avg: { - value: null, - }, - }, - { - key_as_string: '2020-07-05T08:10:00.000Z', - key: 1593936600000, - doc_count: 215, - pct: { - values: { - '95.0': 3653624.0, - '99.0': 5046264.0, - }, - }, - avg: { - value: 397251.288372093, - }, - }, - { - key_as_string: '2020-07-05T08:20:00.000Z', - key: 1593937200000, - doc_count: 494, - pct: { - values: { - '95.0': 3276768.0, - '99.0': 4292576.0, - }, - }, - avg: { - value: 361953.5931174089, - }, - }, - { - key_as_string: '2020-07-05T08:30:00.000Z', - key: 1593937800000, - doc_count: 518, - pct: { - values: { - '95.0': 522208.0, - '99.0': 4128736.0, - }, - }, - avg: { - value: 259173.0694980695, - }, - }, - { - key_as_string: '2020-07-05T08:40:00.000Z', - key: 1593938400000, - doc_count: 449, - pct: { - values: { - '95.0': 372728.0, - '99.0': 843768.0, - }, - }, - avg: { - value: 79648.20935412026, - }, - }, - ], - }, - overall_avg_duration: { - value: 73065.05176360115, - }, - }, -} as unknown) as ESResponse; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts deleted file mode 100644 index bda3bfcdf769c..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts +++ /dev/null @@ -1,153 +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 { timeseriesResponse } from './mock_responses/timeseries_response'; -import { - ApmTimeSeriesResponse, - getTpmBuckets, - timeseriesTransformer, -} from './transform'; - -describe('timeseriesTransformer', () => { - let res: ApmTimeSeriesResponse; - beforeEach(async () => { - res = await timeseriesTransformer({ - timeseriesResponse, - bucketSize: 120, - durationAsMinutes: 10, - }); - }); - - it('should have correct order', () => { - expect(res.tpmBuckets.map((bucket) => bucket.key)).toEqual([ - 'HTTP 2xx', - 'HTTP 3xx', - 'HTTP 4xx', - 'HTTP 5xx', - 'A Custom Bucket (that should be last)', - ]); - }); - - it('should match snapshot', () => { - expect(res).toMatchSnapshot(); - }); -}); - -describe('getTpmBuckets', () => { - it('should return response', () => { - const buckets = [ - { - key: 'HTTP 4xx', - doc_count: 300, - timeseries: { - buckets: [ - { - key_as_string: '', - key: 0, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '', - key: 1, - doc_count: 200, - count: { - value: 200, - }, - }, - { - key_as_string: '', - key: 2, - doc_count: 300, - count: { - value: 300, - }, - }, - { - key_as_string: '', - key: 3, - doc_count: 400, - count: { - value: 400, - }, - }, - ], - }, - }, - { - key: 'HTTP 5xx', - doc_count: 400, - timeseries: { - buckets: [ - { - key_as_string: '', - key: 0, - doc_count: 0, - count: { - value: 0, - }, - }, - { - key_as_string: '', - key: 1, - doc_count: 100, - count: { - value: 100, - }, - }, - { - key_as_string: '', - key: 2, - doc_count: 100, - count: { - value: 100, - }, - }, - { - key_as_string: '', - key: 3, - doc_count: 300, - count: { - value: 300, - }, - }, - ], - }, - }, - ]; - - expect( - getTpmBuckets({ - transactionResultBuckets: buckets, - bucketSize: 120, - durationAsMinutes: 10, - }) - ).toEqual([ - { - avg: 90, - dataPoints: [ - { x: 0, y: 0 }, - { x: 1, y: 100 }, - { x: 2, y: 150 }, - { x: 3, y: 200 }, - ], - key: 'HTTP 4xx', - }, - { - avg: 50, - dataPoints: [ - { x: 0, y: 0 }, - { x: 1, y: 50 }, - { x: 2, y: 50 }, - { x: 3, y: 150 }, - ], - key: 'HTTP 5xx', - }, - ]); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts deleted file mode 100644 index eecb3e7177ef6..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isNumber, sortBy } from 'lodash'; -import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { Coordinate } from '../../../../../typings/timeseries'; -import { ESResponse } from './fetcher'; - -export type ApmTimeSeriesResponse = ReturnType; - -export function timeseriesTransformer({ - timeseriesResponse, - bucketSize, - durationAsMinutes, -}: { - timeseriesResponse: ESResponse; - bucketSize: number; - durationAsMinutes: number; -}) { - const aggs = timeseriesResponse.aggregations; - const overallAvgDuration = aggs?.overall_avg_duration.value || null; - const responseTimeBuckets = aggs?.response_times.buckets || []; - const { avg, p95, p99 } = getResponseTime(responseTimeBuckets); - const transactionResultBuckets = aggs?.transaction_results.buckets || []; - const tpmBuckets = getTpmBuckets({ - transactionResultBuckets, - bucketSize, - durationAsMinutes, - }); - - return { - responseTimes: { - avg, - p95, - p99, - }, - tpmBuckets, - overallAvgDuration, - }; -} - -type TransactionResultBuckets = Required['aggregations']['transaction_results']['buckets']; - -export function getTpmBuckets({ - transactionResultBuckets = [], - bucketSize, - durationAsMinutes, -}: { - transactionResultBuckets: TransactionResultBuckets; - bucketSize: number; - durationAsMinutes: number; -}) { - const buckets = transactionResultBuckets.map( - ({ key: resultKey, timeseries }) => { - const dataPoints = timeseries.buckets.map((bucket) => { - return { - x: bucket.key, - // divide by minutes - y: bucket.count.value / (bucketSize / 60), - }; - }); - - // Handle empty string result keys - const key = - resultKey === '' ? NOT_AVAILABLE_LABEL : (resultKey as string); - - const docCountTotal = timeseries.buckets - .map((bucket) => bucket.count.value) - .reduce((a, b) => a + b, 0); - - // calculate request/minute - const avg = docCountTotal / durationAsMinutes; - - return { key, dataPoints, avg }; - } - ); - - return sortBy( - buckets, - (bucket) => bucket.key.toString().replace(/^HTTP (\d)xx$/, '00$1') // ensure that HTTP 3xx are sorted at the top - ); -} - -type ResponseTimeBuckets = Required['aggregations']['response_times']['buckets']; - -function getResponseTime(responseTimeBuckets: ResponseTimeBuckets = []) { - return responseTimeBuckets.reduce( - (acc, bucket) => { - const { '95.0': p95, '99.0': p99 } = bucket.pct.values; - - acc.avg.push({ x: bucket.key, y: bucket.avg.value }); - acc.p95.push({ x: bucket.key, y: isNumber(p95) ? p95 : null }); - acc.p99.push({ x: bucket.key, y: isNumber(p99) ? p99 : null }); - return acc; - }, - { - avg: [] as Coordinate[], - p95: [] as Coordinate[], - p99: [] as Coordinate[], - } - ); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/index.ts deleted file mode 100644 index d8593612c0582..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Logger } from 'kibana/server'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { getAnomalySeries } from './get_anomaly_data'; -import { getApmTimeseriesData } from './get_timeseries_data'; -import { ApmTimeSeriesResponse } from './get_timeseries_data/transform'; - -function getDates(apmTimeseries: ApmTimeSeriesResponse) { - return apmTimeseries.responseTimes.avg.map((p) => p.x); -} - -export type TimeSeriesAPIResponse = PromiseReturnType< - typeof getTransactionCharts ->; -export async function getTransactionCharts(options: { - serviceName: string; - transactionType: string | undefined; - transactionName: string | undefined; - setup: Setup & SetupTimeRange; - searchAggregatedTransactions: boolean; - logger: Logger; -}) { - const apmTimeseries = await getApmTimeseriesData(options); - const anomalyTimeseries = await getAnomalySeries({ - ...options, - timeSeriesDates: getDates(apmTimeseries), - }); - - return { - apmTimeseries, - anomalyTimeseries, - }; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts rename to x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index aad67c43f48e2..425250fd5a1d1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -5,9 +5,9 @@ */ import { Logger } from 'kibana/server'; -import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; +import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export type ESResponse = Exclude< PromiseReturnType, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/get_ml_bucket_size.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts rename to x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/get_ml_bucket_size.ts index 8ab5b33003b86..1bb9e8a9e77a9 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/get_ml_bucket_size.ts @@ -5,7 +5,7 @@ */ import { Logger } from 'kibana/server'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; interface IOptions { setup: Setup & SetupTimeRange; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts similarity index 72% rename from x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts rename to x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts index e72219a3cbd72..27dd7c0f6970b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts @@ -5,36 +5,42 @@ */ import { Logger } from 'kibana/server'; import { isNumber } from 'lodash'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; -import { getBucketSize } from '../../../helpers/get_bucket_size'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { anomalySeriesFetcher } from './fetcher'; import { getMlBucketSize } from './get_ml_bucket_size'; import { anomalySeriesTransform } from './transform'; -import { getMLJobIds } from '../../../service_map/get_service_anomalies'; +import { getMLJobIds } from '../../service_map/get_service_anomalies'; +import { getLatencyTimeseries } from '../get_latency_charts'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; export async function getAnomalySeries({ serviceName, transactionType, transactionName, - timeSeriesDates, + latencyTimeseries, setup, logger, }: { serviceName: string; transactionType: string | undefined; transactionName: string | undefined; - timeSeriesDates: number[]; + latencyTimeseries: PromiseReturnType< + typeof getLatencyTimeseries + >['latencyTimeseries']; setup: Setup & SetupTimeRange; logger: Logger; }) { - // don't fetch anomalies for transaction details page - if (transactionName) { - return; - } + const timeseriesDates = latencyTimeseries?.avg?.map(({ x }) => x); - // don't fetch anomalies without a type - if (!transactionType) { + /* + * don't fetch: + * - anomalies for transaction details page + * - anomalies without a type + * - timeseries is empty + */ + if (transactionName || !transactionType || !timeseriesDates?.length) { return; } @@ -94,7 +100,7 @@ export async function getAnomalySeries({ esResponse, mlBucketSize, bucketSize, - timeSeriesDates, + timeseriesDates, jobId ); } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/transform.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts rename to x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/transform.ts index b971a1c28397d..3bc9fdbabf9b8 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/transform.ts @@ -5,7 +5,7 @@ */ import { first, last } from 'lodash'; -import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries'; +import { Coordinate, RectCoordinate } from '../../../../typings/timeseries'; import { ESResponse } from './fetcher'; type IBucket = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts similarity index 51% rename from x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts rename to x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index cffec688806b5..35e14fcc4624b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -3,26 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { ESFilter } from '../../../../../../../typings/elasticsearch'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME, TRANSACTION_NAME, - TRANSACTION_RESULT, TRANSACTION_TYPE, -} from '../../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../../../common/utils/range_filter'; +} from '../../../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, -} from '../../../helpers/aggregated_transactions'; -import { getBucketSize } from '../../../helpers/get_bucket_size'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; +} from '../../../lib/helpers/aggregated_transactions'; +import { getBucketSize } from '../../../lib/helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../../lib/helpers/setup_request'; +import { convertLatencyBucketsToCoordinates } from './transform'; + +export type LatencyChartsSearchResponse = PromiseReturnType< + typeof searchLatency +>; -export type ESResponse = PromiseReturnType; -export function timeseriesFetcher({ +async function searchLatency({ serviceName, transactionType, transactionName, @@ -51,11 +53,14 @@ export function timeseriesFetcher({ filter.push({ term: { [TRANSACTION_NAME]: transactionName } }); } - // TODO reimplement these as uiFilters if (transactionType) { filter.push({ term: { [TRANSACTION_TYPE]: transactionType } }); } + const field = getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ); + const params = { apm: { events: [ @@ -68,7 +73,7 @@ export function timeseriesFetcher({ size: 0, query: { bool: { filter } }, aggs: { - response_times: { + latencyTimeseries: { date_histogram: { field: '@timestamp', fixed_interval: intervalString, @@ -76,56 +81,57 @@ export function timeseriesFetcher({ extended_bounds: { min: start, max: end }, }, aggs: { - avg: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, + avg: { avg: { field } }, pct: { percentiles: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), + field, percents: [95, 99], hdr: { number_of_significant_value_digits: 2 }, }, }, }, }, - overall_avg_duration: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - transaction_results: { - terms: { field: TRANSACTION_RESULT, missing: '' }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { min: start, max: end }, - }, - aggs: { - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }, - }, + overall_avg_duration: { avg: { field } }, }, }, }; return apmEventClient.search(params); } + +export async function getLatencyTimeseries({ + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, +}: { + serviceName: string; + transactionType: string | undefined; + transactionName: string | undefined; + setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; +}) { + const response = await searchLatency({ + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, + }); + + if (!response.aggregations) { + return { + latencyTimeseries: { avg: [], p95: [], p99: [] }, + overallAvgDuration: null, + }; + } + + return { + overallAvgDuration: + response.aggregations.overall_avg_duration.value || null, + latencyTimeseries: convertLatencyBucketsToCoordinates( + response.aggregations.latencyTimeseries.buckets + ), + }; +} diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/transform.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/transform.ts new file mode 100644 index 0000000000000..f4d914afc9483 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/transform.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isNumber } from 'lodash'; +import { LatencyChartsSearchResponse } from '.'; +import { Coordinate } from '../../../../typings/timeseries'; + +type LatencyBuckets = Required['aggregations']['latencyTimeseries']['buckets']; + +export function convertLatencyBucketsToCoordinates( + latencyBuckets: LatencyBuckets = [] +) { + return latencyBuckets.reduce( + (acc, bucket) => { + const { '95.0': p95, '99.0': p99 } = bucket.pct.values; + + acc.avg.push({ x: bucket.key, y: bucket.avg.value }); + acc.p95.push({ x: bucket.key, y: isNumber(p95) ? p95 : null }); + acc.p99.push({ x: bucket.key, y: isNumber(p99) ? p99 : null }); + return acc; + }, + { + avg: [] as Coordinate[], + p95: [] as Coordinate[], + p99: [] as Coordinate[], + } + ); +} diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts new file mode 100644 index 0000000000000..e7007f8db0197 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_RESULT, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../../lib/helpers/aggregated_transactions'; +import { getBucketSize } from '../../../lib/helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../../lib/helpers/setup_request'; +import { getThroughputBuckets } from './transform'; + +export type ThroughputChartsResponse = PromiseReturnType< + typeof searchThroughput +>; + +async function searchThroughput({ + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, + intervalString, +}: { + serviceName: string; + transactionType: string | undefined; + transactionName: string | undefined; + setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; + intervalString: string; +}) { + const { start, end, apmEventClient } = setup; + + const filter: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ...setup.esFilter, + ]; + + if (transactionName) { + filter.push({ term: { [TRANSACTION_NAME]: transactionName } }); + } + + if (transactionType) { + filter.push({ term: { [TRANSACTION_TYPE]: transactionType } }); + } + + const field = getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ); + + const params = { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { bool: { filter } }, + aggs: { + throughput: { + terms: { field: TRANSACTION_RESULT, missing: '' }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { count: { value_count: { field } } }, + }, + }, + }, + }, + }, + }; + + return apmEventClient.search(params); +} + +export async function getThroughputCharts({ + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, +}: { + serviceName: string; + transactionType: string | undefined; + transactionName: string | undefined; + setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; +}) { + const { start, end } = setup; + const { bucketSize, intervalString } = getBucketSize({ start, end }); + const durationAsMinutes = (end - start) / 1000 / 60; + + const response = await searchThroughput({ + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, + intervalString, + }); + + return { + throughputTimeseries: getThroughputBuckets({ + throughputResultBuckets: response.aggregations?.throughput.buckets, + bucketSize, + durationAsMinutes, + }), + }; +} diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/transform.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/transform.ts new file mode 100644 index 0000000000000..0ebf1446265fd --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/transform.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 { sortBy } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { ThroughputChartsResponse } from '.'; + +type ThroughputResultBuckets = Required['aggregations']['throughput']['buckets']; + +export function getThroughputBuckets({ + throughputResultBuckets = [], + bucketSize, + durationAsMinutes, +}: { + throughputResultBuckets?: ThroughputResultBuckets; + bucketSize: number; + durationAsMinutes: number; +}) { + const buckets = throughputResultBuckets.map( + ({ key: resultKey, timeseries }) => { + const dataPoints = timeseries.buckets.map((bucket) => { + return { + x: bucket.key, + // divide by minutes + y: bucket.count.value / (bucketSize / 60), + }; + }); + + // Handle empty string result keys + const key = + resultKey === '' ? NOT_AVAILABLE_LABEL : (resultKey as string); + + const docCountTotal = timeseries.buckets + .map((bucket) => bucket.count.value) + .reduce((a, b) => a + b, 0); + + // calculate request/minute + const avg = docCountTotal / durationAsMinutes; + + return { key, dataPoints, avg }; + } + ); + + return sortBy( + buckets, + (bucket) => bucket.key.toString().replace(/^HTTP (\d)xx$/, '00$1') // ensure that HTTP 3xx are sorted at the top + ); +} diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts index eff9451c9e1cd..c903f7daf2b91 100644 --- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts @@ -4,16 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getTransactionBreakdown } from './breakdown'; -import { getTransactionCharts } from './charts'; -import { getTransactionDistribution } from './distribution'; -import { getTransaction } from './get_transaction'; import { - SearchParamsMock, inspectSearchParams, + SearchParamsMock, } from '../../utils/test_helpers'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loggerMock } from '../../../../../../src/core/server/logging/logger.mock'; +import { getTransactionBreakdown } from './breakdown'; +import { getTransactionDistribution } from './distribution'; +import { getTransaction } from './get_transaction'; describe('transaction queries', () => { let mock: SearchParamsMock; @@ -47,49 +44,6 @@ describe('transaction queries', () => { expect(mock.params).toMatchSnapshot(); }); - it('fetches transaction charts', async () => { - mock = await inspectSearchParams((setup) => - getTransactionCharts({ - serviceName: 'foo', - transactionName: undefined, - transactionType: undefined, - setup, - searchAggregatedTransactions: false, - logger: loggerMock.create(), - }) - ); - expect(mock.params).toMatchSnapshot(); - }); - - it('fetches transaction charts for a transaction type', async () => { - mock = await inspectSearchParams((setup) => - getTransactionCharts({ - serviceName: 'foo', - transactionName: 'bar', - transactionType: undefined, - setup, - searchAggregatedTransactions: false, - logger: loggerMock.create(), - }) - ); - expect(mock.params).toMatchSnapshot(); - }); - - it('fetches transaction charts for a transaction type and transaction name', async () => { - mock = await inspectSearchParams((setup) => - getTransactionCharts({ - serviceName: 'foo', - transactionName: 'bar', - transactionType: 'baz', - setup, - searchAggregatedTransactions: false, - logger: loggerMock.create(), - }) - ); - - expect(mock.params).toMatchSnapshot(); - }); - it('fetches transaction distribution', async () => { mock = await inspectSearchParams((setup) => getTransactionDistribution({ diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 0e066a1959c49..d34e67083b037 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -23,6 +23,7 @@ import { serviceAnnotationsCreateRoute, serviceErrorGroupsRoute, serviceThroughputRoute, + serviceDependenciesRoute, } from './services'; import { agentConfigurationRoute, @@ -52,12 +53,13 @@ import { } from './correlations'; import { transactionChartsBreakdownRoute, - transactionChartsRoute, transactionChartsDistributionRoute, transactionChartsErrorRateRoute, transactionGroupsRoute, transactionGroupsOverviewRoute, -} from './transactions/transactions_routes'; + transactionLatencyChatsRoute, + transactionThroughputChatsRoute, +} from './transactions'; import { errorGroupsLocalFiltersRoute, metricsLocalFiltersRoute, @@ -121,6 +123,7 @@ const createApmApi = () => { .add(serviceAnnotationsCreateRoute) .add(serviceErrorGroupsRoute) .add(serviceThroughputRoute) + .add(serviceDependenciesRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) @@ -152,11 +155,12 @@ const createApmApi = () => { // Transactions .add(transactionChartsBreakdownRoute) - .add(transactionChartsRoute) .add(transactionChartsDistributionRoute) .add(transactionChartsErrorRateRoute) .add(transactionGroupsRoute) .add(transactionGroupsOverviewRoute) + .add(transactionLatencyChatsRoute) + .add(transactionThroughputChatsRoute) // UI filters .add(errorGroupsLocalFiltersRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index a82f1b64d5537..40ad7fdd05248 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -18,6 +18,7 @@ import { getServiceAnnotations } from '../lib/services/annotations'; import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; +import { getServiceDependencies } from '../lib/services/get_service_dependencies'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; import { getThroughput } from '../lib/services/get_throughput'; @@ -275,3 +276,32 @@ export const serviceThroughputRoute = createRoute({ }); }, }); + +export const serviceDependenciesRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/dependencies', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + rangeRt, + t.type({ environment: t.string, numBuckets: toNumberRt }), + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { serviceName } = context.params.path; + const { environment, numBuckets } = context.params.query; + + return getServiceDependencies({ + serviceName, + environment, + setup, + numBuckets, + }); + }, +}); diff --git a/x-pack/plugins/apm/server/routes/transactions/transactions_routes.ts b/x-pack/plugins/apm/server/routes/transactions.ts similarity index 72% rename from x-pack/plugins/apm/server/routes/transactions/transactions_routes.ts rename to x-pack/plugins/apm/server/routes/transactions.ts index 11d247ccab84f..9b7a0981a4fed 100644 --- a/x-pack/plugins/apm/server/routes/transactions/transactions_routes.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -6,17 +6,19 @@ import Boom from '@hapi/boom'; import * as t from 'io-ts'; -import { toNumberRt } from '../../../common/runtime_types/to_number_rt'; -import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; -import { setupRequest } from '../../lib/helpers/setup_request'; -import { getServiceTransactionGroups } from '../../lib/services/get_service_transaction_groups'; -import { getTransactionBreakdown } from '../../lib/transactions/breakdown'; -import { getTransactionCharts } from '../../lib/transactions/charts'; -import { getTransactionDistribution } from '../../lib/transactions/distribution'; -import { getTransactionGroupList } from '../../lib/transaction_groups'; -import { getErrorRate } from '../../lib/transaction_groups/get_error_rate'; -import { createRoute } from '../create_route'; -import { rangeRt, uiFiltersRt } from '../default_api_types'; +import { createRoute } from './create_route'; +import { rangeRt, uiFiltersRt } from './default_api_types'; +import { toNumberRt } from '../../common/runtime_types/to_number_rt'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { getServiceTransactionGroups } from '../lib/services/get_service_transaction_groups'; +import { getTransactionBreakdown } from '../lib/transactions/breakdown'; +import { getAnomalySeries } from '../lib/transactions/get_anomaly_data'; +import { getTransactionDistribution } from '../lib/transactions/distribution'; +import { getTransactionGroupList } from '../lib/transaction_groups'; +import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; +import { getLatencyTimeseries } from '../lib/transactions/get_latency_charts'; +import { getThroughputCharts } from '../lib/transactions/get_throughput_charts'; /** * Returns a list of transactions grouped by name @@ -107,15 +109,8 @@ export const transactionGroupsOverviewRoute = createRoute({ }, }); -/** - * Returns timeseries for latency, throughput and anomalies - * TODO: break it into 3 new APIs: - * - Latency: /transactions/charts/latency - * - Throughput: /transactions/charts/throughput - * - anomalies: /transactions/charts/anomaly - */ -export const transactionChartsRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts', +export const transactionLatencyChatsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/latency', params: t.type({ path: t.type({ serviceName: t.string, @@ -152,10 +147,62 @@ export const transactionChartsRoute = createRoute({ transactionName, setup, searchAggregatedTransactions, - logger, }; - return getTransactionCharts(options); + const { + latencyTimeseries, + overallAvgDuration, + } = await getLatencyTimeseries(options); + + const anomalyTimeseries = await getAnomalySeries({ + ...options, + logger, + latencyTimeseries, + }); + + return { latencyTimeseries, overallAvgDuration, anomalyTimeseries }; + }, +}); + +export const transactionThroughputChatsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/transactions/charts/throughput', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + t.partial({ + transactionType: t.string, + transactionName: t.string, + }), + uiFiltersRt, + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + const { transactionType, transactionName } = context.params.query; + + if (!setup.uiFilters.environment) { + throw Boom.badRequest( + `environment is a required property of the ?uiFilters JSON for transaction_groups/charts.` + ); + } + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return await getThroughputCharts({ + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, + }); }, }); diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/metric_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/metric_raw.ts index b4a1954783db0..b4f1576ed03c0 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/metric_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/metric_raw.ts @@ -89,11 +89,28 @@ type TransactionDurationMetric = BaseMetric & { kubernetes?: Kubernetes; }; +export type SpanDestinationMetric = BaseMetric & { + span: { + destination: { + service: { + resource: string; + response_time: { + count: number; + sum: { + us: number; + }; + }; + }; + }; + }; +}; + export type MetricRaw = | BaseMetric | TransactionBreakdownMetric | SpanBreakdownMetric | TransactionDurationMetric + | SpanDestinationMetric | SystemMetric | CGroupMetric | JVMMetric; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index dcb3dc02f6519..e152ed23af1b3 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -20,6 +20,11 @@ export interface SpanRaw extends APMBaseDoc { name: string; }; span: { + destination?: { + service: { + resource: string; + }; + }; action?: string; duration: { us: number }; id: string; diff --git a/x-pack/plugins/fleet/common/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts index cd990d70c3612..30b52bcb28748 100644 --- a/x-pack/plugins/fleet/common/services/agent_status.ts +++ b/x-pack/plugins/fleet/common/services/agent_status.ts @@ -62,6 +62,14 @@ export function buildKueryForOfflineAgents() { }s AND not (${buildKueryForErrorAgents()})`; } -export function buildKueryForUpdatingAgents() { +export function buildKueryForUpgradingAgents() { return `${AGENT_SAVED_OBJECT_TYPE}.upgrade_started_at:*`; } + +export function buildKueryForUpdatingAgents() { + return `(${buildKueryForUpgradingAgents()}) or (${buildKueryForEnrollingAgents()}) or (${buildKueryForUnenrollingAgents()})`; +} + +export function buildKueryForInactiveAgents() { + return `${AGENT_SAVED_OBJECT_TYPE}.active:false`; +} diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 872b389d248a3..59fab14f90e6e 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -22,6 +22,8 @@ export type AgentStatus = | 'updating' | 'degraded'; +export type SimplifiedAgentStatus = 'healthy' | 'unhealthy' | 'updating' | 'offline' | 'inactive'; + export type AgentActionType = | 'POLICY_CHANGE' | 'UNENROLL' diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 66a2a58a25ac5..96868fa8cfc3b 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -222,6 +222,7 @@ export interface RegistryElasticsearch { 'index_template.mappings'?: object; } +export type RegistryVarType = 'integer' | 'bool' | 'password' | 'text' | 'yaml'; // EPR types this as `[]map[string]interface{}` // which means the official/possible type is Record // but we effectively only see this shape @@ -229,7 +230,7 @@ export interface RegistryVarsEntry { name: string; title?: string; description?: string; - type: string; + type: RegistryVarType; required?: boolean; show_user?: boolean; multi?: boolean; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index da7d126c4ecd3..236fc586bf528 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -206,6 +206,7 @@ export interface UpdateAgentRequest { export interface GetAgentStatusRequest { query: { + kuery?: string; policyId?: string; }; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx index 3430a4eb5b258..6cd701da61e26 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx @@ -8,6 +8,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui'; import { TutorialModuleNoticeComponent } from 'src/plugins/home/public'; import { useGetPackages, useLink, useCapabilities } from '../../hooks'; +import { pkgKeyFromPackageInfo } from '../../services/pkg_key_from_package_info'; const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }) => { const { getHref } = useLink(); @@ -41,7 +42,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName } availableAsIntegrationLink: ( void; + onChange: (newValue: string, submit?: boolean) => void; placeholder?: string; } @@ -40,135 +28,73 @@ export const SearchBar: React.FunctionComponent = ({ onChange, placeholder, }) => { - const { suggestions } = useSuggestions(fieldPrefix, value); - - // TODO fix type when correctly typed in EUI - const onAutocompleteClick = (suggestion: any) => { - onChange( - [value.slice(0, suggestion.start), suggestion.value, value.slice(suggestion.end, -1)].join('') - ); - }; - // TODO fix type when correctly typed in EUI - const onChangeSearch = (e: any) => { - onChange(e.value); - }; - - return ( - { - return { - ...suggestion, - // For type - onClick: () => {}, - descriptionDisplay: 'wrap', - labelWidth: '40', - }; - })} - /> - ); -}; - -export function transformSuggestionType(type: string): { iconType: string; color: string } { - switch (type) { - case 'field': - return { iconType: 'kqlField', color: 'tint4' }; - case 'value': - return { iconType: 'kqlValue', color: 'tint0' }; - case 'conjunction': - return { iconType: 'kqlSelector', color: 'tint3' }; - case 'operator': - return { iconType: 'kqlOperand', color: 'tint1' }; - default: - return { iconType: 'kqlOther', color: 'tint1' }; - } -} - -function useSuggestions(fieldPrefix: string, search: string) { const { data } = useStartServices(); + const [indexPatternFields, setIndexPatternFields] = useState(); - const debouncedSearch = useDebounce(search, DEBOUNCE_SEARCH_MS); - const [suggestions, setSuggestions] = useState([]); + const isQueryValid = useMemo(() => { + if (!value || value === '') { + return true; + } - const fetchSuggestions = async () => { try { - const res = (await data.indexPatterns.getFieldsForWildcard({ - pattern: INDEX_NAME, - })) as IFieldType[]; - if (!data || !data.autocomplete) { - throw new Error('Missing data plugin'); - } - const query = debouncedSearch || ''; - // @ts-ignore - const esSuggestions = ( - await data.autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [ - { - title: INDEX_NAME, - fields: res, - }, - ], - boolFilter: [], - query, - selectionStart: query.length, - selectionEnd: query.length, - }) - ) - .filter((suggestion) => { - if (suggestion.type === 'conjunction') { - return true; - } - if (suggestion.type === 'value') { - return true; - } - if (suggestion.type === 'operator') { - return true; - } + esKuery.fromKueryExpression(value); + return true; + } catch (e) { + return false; + } + }, [value]); - if (fieldPrefix && suggestion.text.startsWith(fieldPrefix)) { + useEffect(() => { + const fetchFields = async () => { + try { + const _fields: IFieldType[] = await data.indexPatterns.getFieldsForWildcard({ + pattern: INDEX_NAME, + }); + const fields = (_fields || []).filter((field) => { + if (fieldPrefix && field.name.startsWith(fieldPrefix)) { for (const hiddenField of HIDDEN_FIELDS) { - if (suggestion.text.startsWith(hiddenField)) { + if (field.name.startsWith(hiddenField)) { return false; } } return true; } + }); + setIndexPatternFields(fields); + } catch (err) { + setIndexPatternFields(undefined); + } + }; + fetchFields(); + }, [data.indexPatterns, fieldPrefix]); - return false; - }) - .map((suggestion: any) => ({ - label: suggestion.text, - description: suggestion.description || '', - type: transformSuggestionType(suggestion.type), - start: suggestion.start, - end: suggestion.end, - value: suggestion.text, - })); - - setSuggestions(esSuggestions); - } catch (err) { - setSuggestions([]); - } - }; - - useEffect(() => { - fetchSuggestions(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearch]); - - return { - suggestions, - }; -} + return ( + { + onChange(newQuery.query as string); + }} + onSubmit={(newQuery) => { + onChange(newQuery.query as string, true); + }} + /> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts index 2fce7f8f5e825..5c2194a3e37cd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts @@ -18,6 +18,7 @@ export type StaticPage = export type DynamicPage = | 'integration_details' + | 'integration_policy_edit' | 'policy_details' | 'add_integration_from_policy' | 'add_integration_to_policy' @@ -41,6 +42,7 @@ export const PAGE_ROUTING_PATHS = { integrations_all: '/integrations', integrations_installed: '/integrations/installed', integration_details: '/integrations/detail/:pkgkey/:panel?', + integration_policy_edit: '/integrations/edit-integration/:packagePolicyId', policies: '/policies', policies_list: '/policies', policy_details: '/policies/:policyId/:tabId?', @@ -69,6 +71,8 @@ export const pagePathGetters: { integrations_installed: () => '/integrations/installed', integration_details: ({ pkgkey, panel }) => `/integrations/detail/${pkgkey}${panel ? `/${panel}` : ''}`, + integration_policy_edit: ({ packagePolicyId }) => + `/integrations/edit-integration/${packagePolicyId}`, policies: () => '/policies', policies_list: () => '/policies', policy_details: ({ policyId, tabId }) => `/policies/${policyId}${tabId ? `/${tabId}` : ''}`, diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index 40654645ecd3f..4feff29896459 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -73,6 +73,20 @@ const breadcrumbGetters: { }, { text: pkgTitle }, ], + integration_policy_edit: ({ pkgTitle, pkgkey, policyName }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.fleet.breadcrumbs.integrationPageTitle', { + defaultMessage: 'Integration', + }), + }, + { + href: pagePathGetters.integration_details({ pkgkey, panel: 'policies' }), + text: pkgTitle, + }, + { text: policyName }, + ], policies: () => [ BASE_BREADCRUMB, { diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts index 7bbf621c57894..b6a3ecfde78d6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/agents.ts @@ -62,13 +62,10 @@ export function useGetAgents(query: GetAgentsRequest['query'], options?: Request }); } -export function sendGetAgentStatus( - query: GetAgentStatusRequest['query'], - options?: RequestOptions -) { - return sendRequest({ +export function sendGetAgents(query: GetAgentsRequest['query'], options?: RequestOptions) { + return sendRequest({ method: 'get', - path: agentRouteService.getStatusPath(), + path: agentRouteService.getListPath(), query, ...options, }); @@ -83,6 +80,18 @@ export function useGetAgentStatus(query: GetAgentStatusRequest['query'], options }); } +export function sendGetAgentStatus( + query: GetAgentStatusRequest['query'], + options?: RequestOptions +) { + return sendRequest({ + method: 'get', + path: agentRouteService.getStatusPath(), + query, + ...options, + }); +} + export function sendPutAgentReassign( agentId: string, body: PutAgentReassignRequest['body'], diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx index 9188f0069b8bf..cac133acd4d2d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -38,7 +38,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ 'data-test-subj': dataTestSubj, }) => { const pageTitle = useMemo(() => { - if ((from === 'package' || from === 'edit') && packageInfo) { + if ((from === 'package' || from === 'package-edit' || from === 'edit') && packageInfo) { return ( @@ -76,7 +76,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ ); } - return from === 'edit' ? ( + return from === 'edit' || from === 'package-edit' ? (

{ - return from === 'edit' ? ( + return from === 'edit' || from === 'package-edit' ? ( { if (multi) { @@ -59,6 +67,18 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ /> ); } + if (type === 'bool') { + return ( + onChange(e.target.checked)} + onBlur={() => setIsDirty(true)} + /> + ); + } + return ( setIsDirty(true)} /> ); - }, [isInvalid, multi, onChange, type, value]); + }, [isInvalid, multi, onChange, type, value, fieldLabel]); + + // Boolean cannot be optional by default set to false + const isOptional = type !== 'bool' && !required; return ( { ? packageInfo && ( ) : agentPolicy && ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index f6533a06cea27..b7de9d0afe8f5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -20,6 +20,7 @@ import { AgentPolicy, PackageInfo, PackagePolicy, NewPackagePolicy } from '../.. import { packageToPackagePolicyInputs } from '../../../services'; import { Loading } from '../../../components'; import { PackagePolicyValidationResults } from './services'; +import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; export const StepDefinePackagePolicy: React.FunctionComponent<{ agentPolicy: AgentPolicy; @@ -34,8 +35,8 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ // Update package policy's package and agent policy info useEffect(() => { const pkg = packagePolicy.package; - const currentPkgKey = pkg ? `${pkg.name}-${pkg.version}` : ''; - const pkgKey = `${packageInfo.name}-${packageInfo.version}`; + const currentPkgKey = pkg ? pkgKeyFromPackageInfo(pkg) : ''; + const pkgKey = pkgKeyFromPackageInfo(packageInfo); // If package has changed, create shell package policy with input&stream values based on package info if (currentPkgKey !== pkgKey) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx index 8c646323c312c..3bcafaecbf8d9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx @@ -16,6 +16,7 @@ import { sendGetPackageInfoByKey, } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; +import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; export const StepSelectPackage: React.FunctionComponent<{ agentPolicyId: string; @@ -32,7 +33,7 @@ export const StepSelectPackage: React.FunctionComponent<{ }) => { // Selected package state const [selectedPkgKey, setSelectedPkgKey] = useState( - packageInfo ? `${packageInfo.name}-${packageInfo.version}` : undefined + packageInfo ? pkgKeyFromPackageInfo(packageInfo) : undefined ); const [selectedPkgError, setSelectedPkgError] = useState(); @@ -92,7 +93,7 @@ export const StepSelectPackage: React.FunctionComponent<{ updatePackageInfo(undefined); } }; - if (!packageInfo || selectedPkgKey !== `${packageInfo.name}-${packageInfo.version}`) { + if (!packageInfo || selectedPkgKey !== pkgKeyFromPackageInfo(packageInfo)) { fetchPackageInfo(); } }, [selectedPkgKey, packageInfo, updatePackageInfo, setIsLoadingSecondStep]); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts index c6e16c2cb4d97..7eb5d95c1ab05 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export type CreatePackagePolicyFrom = 'package' | 'policy' | 'edit'; +export type CreatePackagePolicyFrom = 'package' | 'package-edit' | 'policy' | 'edit'; export type PackagePolicyFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 8f798445b2362..26f99bd88a923 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.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, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, memo } from 'react'; import { useRouteMatch, useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -45,15 +45,24 @@ import { useUIExtension } from '../../../hooks/use_ui_extension'; import { ExtensionWrapper } from '../../../components/extension_wrapper'; import { GetOnePackagePolicyResponse } from '../../../../../../common/types/rest_spec'; import { PackagePolicyEditExtensionComponentProps } from '../../../types'; +import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; -export const EditPackagePolicyPage: React.FunctionComponent = () => { +export const EditPackagePolicyPage = memo(() => { + const { + params: { packagePolicyId }, + } = useRouteMatch<{ policyId: string; packagePolicyId: string }>(); + + return ; +}); + +export const EditPackagePolicyForm = memo<{ + packagePolicyId: string; + from?: CreatePackagePolicyFrom; +}>(({ packagePolicyId, from = 'edit' }) => { const { notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); - const { - params: { policyId, packagePolicyId }, - } = useRouteMatch<{ policyId: string; packagePolicyId: string }>(); const history = useHistory(); const { getHref, getPath } = useLink(); @@ -76,16 +85,31 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { GetOnePackagePolicyResponse['item'] >(); + const policyId = agentPolicy?.id ?? ''; + // Retrieve agent policy, package, and package policy info useEffect(() => { const getData = async () => { setIsLoadingData(true); setLoadingError(undefined); try { - const [{ data: agentPolicyData }, { data: packagePolicyData }] = await Promise.all([ - sendGetOneAgentPolicy(policyId), - sendGetOnePackagePolicy(packagePolicyId), - ]); + const { + data: packagePolicyData, + error: packagePolicyError, + } = await sendGetOnePackagePolicy(packagePolicyId); + + if (packagePolicyError) { + throw packagePolicyError; + } + + const { data: agentPolicyData, error: agentPolicyError } = await sendGetOneAgentPolicy( + packagePolicyData!.item.policy_id + ); + + if (agentPolicyError) { + throw agentPolicyError; + } + if (agentPolicyData?.item) { setAgentPolicy(agentPolicyData.item); } @@ -108,7 +132,8 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { const newPackagePolicy = { ...restOfPackagePolicy, inputs: inputs.map((input) => { - const { streams, ...restOfInput } = input; + // Remove `compiled_input` from all input info, we assign this after saving + const { streams, compiled_input: compiledInput, ...restOfInput } = input; return { ...restOfInput, streams: streams.map((stream) => { @@ -122,7 +147,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { setPackagePolicy(newPackagePolicy); if (packagePolicyData.item.package) { const { data: packageData } = await sendGetPackageInfoByKey( - `${packagePolicyData.item.package.name}-${packagePolicyData.item.package.version}` + pkgKeyFromPackageInfo(packagePolicyData.item.package) ); if (packageData?.response) { setPackageInfo(packageData.response); @@ -149,7 +174,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { } }; - if (isFleetEnabled) { + if (isFleetEnabled && policyId) { getAgentCount(); } }, [policyId, isFleetEnabled]); @@ -213,8 +238,32 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { [updatePackagePolicy] ); - // Cancel url - const cancelUrl = getHref('policy_details', { policyId }); + // Cancel url + Success redirect Path: + // if `from === 'edit'` then it links back to Policy Details + // if `from === 'package-edit'` then it links back to the Integration Policy List + const cancelUrl = useMemo((): string => { + if (packageInfo && policyId) { + return from === 'package-edit' + ? getHref('integration_details', { + pkgkey: pkgKeyFromPackageInfo(packageInfo!), + panel: 'policies', + }) + : getHref('policy_details', { policyId }); + } + return '/'; + }, [from, getHref, packageInfo, policyId]); + + const successRedirectPath = useMemo(() => { + if (packageInfo && policyId) { + return from === 'package-edit' + ? getPath('integration_details', { + pkgkey: pkgKeyFromPackageInfo(packageInfo!), + panel: 'policies', + }) + : getPath('policy_details', { policyId }); + } + return '/'; + }, [from, getPath, packageInfo, policyId]); // Save package policy const [formState, setFormState] = useState('INVALID'); @@ -236,7 +285,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { } const { error } = await savePackagePolicy(); if (!error) { - history.push(getPath('policy_details', { policyId })); + history.push(successRedirectPath); notifications.toasts.addSuccess({ title: i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationTitle', { defaultMessage: `Successfully updated '{packagePolicyName}'`, @@ -286,7 +335,7 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { }; const layoutProps = { - from: 'edit' as CreatePackagePolicyFrom, + from, cancelUrl, agentPolicy, packageInfo, @@ -362,13 +411,21 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { error={ loadingError || i18n.translate('xpack.fleet.editPackagePolicy.errorLoadingDataMessage', { - defaultMessage: 'There was an error loading this intergration information', + defaultMessage: 'There was an error loading this integration information', }) } /> ) : ( <> - + {from === 'package' || from === 'package-edit' ? ( + + ) : ( + + )} {formState === 'CONFIRM' && ( { )} ); -}; +}); -const Breadcrumb: React.FunctionComponent<{ policyName: string; policyId: string }> = ({ +const PoliciesBreadcrumb: React.FunctionComponent<{ policyName: string; policyId: string }> = ({ policyName, policyId, }) => { useBreadcrumbs('edit_integration', { policyName, policyId }); return null; }; + +const IntegrationsBreadcrumb = memo<{ + pkgTitle: string; + policyName: string; + pkgkey: string; +}>(({ pkgTitle, policyName, pkgkey }) => { + useBreadcrumbs('integration_policy_edit', { policyName, pkgTitle, pkgkey }); + return null; +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx new file mode 100644 index 0000000000000..baea6d364e586 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.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, { useState } from 'react'; +import { + EuiFilterButton, + EuiFilterGroup, + EuiFilterSelectItem, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AgentPolicy } from '../../../../types'; +import { SearchBar } from '../../../../components'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../../../constants'; + +const statusFilters = [ + { + status: 'healthy', + label: i18n.translate('xpack.fleet.agentList.statusHealthyFilterText', { + defaultMessage: 'Healthy', + }), + }, + { + status: 'unhealthy', + label: i18n.translate('xpack.fleet.agentList.statusUnhealthyFilterText', { + defaultMessage: 'Unhealthy', + }), + }, + { + status: 'updating', + label: i18n.translate('xpack.fleet.agentList.statusUpdatingFilterText', { + defaultMessage: 'Updating', + }), + }, + { + status: 'offline', + label: i18n.translate('xpack.fleet.agentList.statusOfflineFilterText', { + defaultMessage: 'Offline', + }), + }, + { + status: 'inactive', + label: i18n.translate('xpack.fleet.agentList.statusInactiveFilterText', { + defaultMessage: 'Inactive', + }), + }, +]; + +export const SearchAndFilterBar: React.FunctionComponent<{ + agentPolicies: AgentPolicy[]; + draftKuery: string; + onDraftKueryChange: (kuery: string) => void; + onSubmitSearch: (kuery: string) => void; + selectedAgentPolicies: string[]; + onSelectedAgentPoliciesChange: (selectedPolicies: string[]) => void; + selectedStatus: string[]; + onSelectedStatusChange: (selectedStatus: string[]) => void; + showUpgradeable: boolean; + onShowUpgradeableChange: (showUpgradeable: boolean) => void; +}> = ({ + agentPolicies, + draftKuery, + onDraftKueryChange, + onSubmitSearch, + selectedAgentPolicies, + onSelectedAgentPoliciesChange, + selectedStatus, + onSelectedStatusChange, + showUpgradeable, + onShowUpgradeableChange, +}) => { + // Policies state for filtering + const [isAgentPoliciesFilterOpen, setIsAgentPoliciesFilterOpen] = useState(false); + + // Status for filtering + const [isStatusFilterOpen, setIsStatutsFilterOpen] = useState(false); + + // Add a agent policy id to current search + const addAgentPolicyFilter = (policyId: string) => { + onSelectedAgentPoliciesChange([...selectedAgentPolicies, policyId]); + }; + + // Remove a agent policy id from current search + const removeAgentPolicyFilter = (policyId: string) => { + onSelectedAgentPoliciesChange( + selectedAgentPolicies.filter((agentPolicy) => agentPolicy !== policyId) + ); + }; + + return ( + <> + {/* Search and filter bar */} + + + + + { + onDraftKueryChange(newSearch); + if (submit) { + onSubmitSearch(newSearch); + } + }} + fieldPrefix={AGENT_SAVED_OBJECT_TYPE} + /> + + + + setIsStatutsFilterOpen(!isStatusFilterOpen)} + isSelected={isStatusFilterOpen} + hasActiveFilters={selectedStatus.length > 0} + numActiveFilters={selectedStatus.length} + disabled={agentPolicies.length === 0} + > + + + } + isOpen={isStatusFilterOpen} + closePopover={() => setIsStatutsFilterOpen(false)} + panelPaddingSize="none" + > +
+ {statusFilters.map(({ label, status }, idx) => ( + { + if (selectedStatus.includes(status)) { + onSelectedStatusChange([...selectedStatus.filter((s) => s !== status)]); + } else { + onSelectedStatusChange([...selectedStatus, status]); + } + }} + > + {label} + + ))} +
+
+ setIsAgentPoliciesFilterOpen(!isAgentPoliciesFilterOpen)} + isSelected={isAgentPoliciesFilterOpen} + hasActiveFilters={selectedAgentPolicies.length > 0} + numActiveFilters={selectedAgentPolicies.length} + numFilters={agentPolicies.length} + disabled={agentPolicies.length === 0} + > + + + } + isOpen={isAgentPoliciesFilterOpen} + closePopover={() => setIsAgentPoliciesFilterOpen(false)} + panelPaddingSize="none" + > +
+ {agentPolicies.map((agentPolicy, index) => ( + { + if (selectedAgentPolicies.includes(agentPolicy.id)) { + removeAgentPolicyFilter(agentPolicy.id); + } else { + addAgentPolicyFilter(agentPolicy.id); + } + }} + > + {agentPolicy.name} + + ))} +
+
+ { + onShowUpgradeableChange(!showUpgradeable); + }} + > + + +
+
+
+
+
+ + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx new file mode 100644 index 0000000000000..250b021c77c15 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiHealth, EuiNotificationBadge, EuiFlexItem } from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import { + AGENT_STATUSES, + getColorForAgentStatus, + getLabelForAgentStatus, +} from '../../services/agent_status'; +import { SimplifiedAgentStatus } from '../../../../types'; + +export const AgentStatusBadges: React.FC<{ + showInactive?: boolean; + agentStatus: { [k in SimplifiedAgentStatus]: number }; +}> = memo(({ agentStatus, showInactive }) => { + const agentStatuses = useMemo(() => { + return AGENT_STATUSES.filter((status) => (showInactive ? true : status !== 'inactive')); + }, [showInactive]); + + return ( + + {agentStatuses.map((status) => ( + + + + ))} + + ); +}); + +const AgentStatusBadge: React.FC<{ status: SimplifiedAgentStatus; count: number }> = memo( + ({ status, count }) => { + return ( + <> + + + {getLabelForAgentStatus(status)} + + + {count} + + + + + + ); + } +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx new file mode 100644 index 0000000000000..b2fa2eacbd5f2 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled from 'styled-components'; +import { EuiColorPaletteDisplay } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { AGENT_STATUSES, getColorForAgentStatus } from '../../services/agent_status'; +import { SimplifiedAgentStatus } from '../../../../types'; + +const StyledEuiColorPaletteDisplay = styled(EuiColorPaletteDisplay)` + &.ingest-agent-status-bar { + border: none; + border-radius: 0; + &:after { + border: none; + } + } +`; + +export const AgentStatusBar: React.FC<{ + agentStatus: { [k in SimplifiedAgentStatus]: number }; +}> = ({ agentStatus }) => { + const palette = useMemo(() => { + return AGENT_STATUSES.reduce((acc, status) => { + const previousStop = acc.length > 0 ? acc[acc.length - 1].stop : 0; + acc.push({ + stop: previousStop + (agentStatus[status] || 0), + color: getColorForAgentStatus(status), + }); + return acc; + }, [] as Array<{ stop: number; color: string }>); + }, [agentStatus]); + return ( + <> + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx new file mode 100644 index 0000000000000..80ab76ffde4a0 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/table_header.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { Agent, SimplifiedAgentStatus } from '../../../../types'; + +import { AgentStatusBar } from './status_bar'; +import { AgentBulkActions } from './bulk_actions'; +import {} from '@elastic/eui'; +import { AgentStatusBadges } from './status_badges'; + +export type SelectionMode = 'manual' | 'query'; + +export const AgentTableHeader: React.FunctionComponent<{ + agentStatus?: { [k in SimplifiedAgentStatus]: number }; + showInactive: boolean; + totalAgents: number; + totalInactiveAgents: number; + selectableAgents: number; + selectionMode: SelectionMode; + setSelectionMode: (mode: SelectionMode) => void; + currentQuery: string; + selectedAgents: Agent[]; + setSelectedAgents: (agents: Agent[]) => void; + refreshAgents: () => void; +}> = ({ + agentStatus, + totalAgents, + totalInactiveAgents, + selectableAgents, + selectionMode, + setSelectionMode, + currentQuery, + selectedAgents, + setSelectedAgents, + refreshAgents, + showInactive, +}) => { + return ( + <> + + + + + + {agentStatus && ( + + )} + + + + {agentStatus && } + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 1d08a1f791976..2067a2bd91c58 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -3,41 +3,38 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useMemo, useCallback, useRef } from 'react'; +import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { EuiBasicTable, EuiButton, EuiEmptyPrompt, - EuiFilterButton, - EuiFilterGroup, - EuiFilterSelectItem, EuiFlexGroup, EuiFlexItem, EuiLink, - EuiPopover, EuiSpacer, EuiText, EuiContextMenuItem, EuiIcon, EuiPortal, - EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; import { AgentEnrollmentFlyout } from '../components'; -import { Agent, AgentPolicy } from '../../../types'; +import { Agent, AgentPolicy, SimplifiedAgentStatus } from '../../../types'; import { usePagination, useCapabilities, useGetAgentPolicies, - useGetAgents, + sendGetAgents, + sendGetAgentStatus, useUrlParams, useLink, useBreadcrumbs, useLicense, useKibanaVersion, + useStartServices, } from '../../../hooks'; -import { SearchBar, ContextMenuActions } from '../../../components'; +import { ContextMenuActions } from '../../../components'; import { AgentStatusKueryHelper, isAgentUpgradeable } from '../../../services'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { @@ -46,37 +43,11 @@ import { AgentUnenrollAgentModal, AgentUpgradeAgentModal, } from '../components'; -import { AgentBulkActions, SelectionMode } from './components/bulk_actions'; - -const REFRESH_INTERVAL_MS = 5000; - -const statusFilters = [ - { - status: 'online', - label: i18n.translate('xpack.fleet.agentList.statusOnlineFilterText', { - defaultMessage: 'Online', - }), - }, - { - status: 'offline', - label: i18n.translate('xpack.fleet.agentList.statusOfflineFilterText', { - defaultMessage: 'Offline', - }), - }, - , - { - status: 'error', - label: i18n.translate('xpack.fleet.agentList.statusErrorFilterText', { - defaultMessage: 'Error', - }), - }, - { - status: 'updating', - label: i18n.translate('xpack.fleet.agentList.statusUpdatingFilterText', { - defaultMessage: 'Updating', - }), - }, -] as Array<{ label: string; status: string }>; +import { AgentTableHeader } from './components/table_header'; +import { SelectionMode } from './components/bulk_actions'; +import { SearchAndFilterBar } from './components/search_and_filter_bar'; + +const REFRESH_INTERVAL_MS = 10000; const RowActions = React.memo<{ agent: Agent; @@ -160,6 +131,7 @@ function safeMetadata(val: any) { } export const AgentListPage: React.FunctionComponent<{}> = () => { + const { notifications } = useStartServices(); useBreadcrumbs('fleet_agent_list'); const { getHref } = useLink(); const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; @@ -168,50 +140,43 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const kibanaVersion = useKibanaVersion(); // Agent data states - const [showInactive, setShowInactive] = useState(false); const [showUpgradeable, setShowUpgradeable] = useState(false); // Table and search states + const [draftKuery, setDraftKuery] = useState(defaultKuery); const [search, setSearch] = useState(defaultKuery); const [selectionMode, setSelectionMode] = useState('manual'); const [selectedAgents, setSelectedAgents] = useState([]); const tableRef = useRef>(null); const { pagination, pageSizeOptions, setPagination } = usePagination(); + const onSubmitSearch = useCallback( + (newKuery: string) => { + setSearch(newKuery); + setPagination({ + ...pagination, + currentPage: 1, + }); + }, + [setSearch, pagination, setPagination] + ); + // Policies state for filtering - const [isAgentPoliciesFilterOpen, setIsAgentPoliciesFilterOpen] = useState(false); const [selectedAgentPolicies, setSelectedAgentPolicies] = useState([]); // Status for filtering - const [isStatusFilterOpen, setIsStatutsFilterOpen] = useState(false); const [selectedStatus, setSelectedStatus] = useState([]); const isUsingFilter = - search.trim() || - selectedAgentPolicies.length || - selectedStatus.length || - showInactive || - showUpgradeable; + search.trim() || selectedAgentPolicies.length || selectedStatus.length || showUpgradeable; const clearFilters = useCallback(() => { + setDraftKuery(''); setSearch(''); setSelectedAgentPolicies([]); setSelectedStatus([]); - setShowInactive(false); setShowUpgradeable(false); - }, [setSearch, setSelectedAgentPolicies, setSelectedStatus, setShowInactive, setShowUpgradeable]); - - // Add a agent policy id to current search - const addAgentPolicyFilter = (policyId: string) => { - setSelectedAgentPolicies([...selectedAgentPolicies, policyId]); - }; - - // Remove a agent policy id from current search - const removeAgentPolicyFilter = (policyId: string) => { - setSelectedAgentPolicies( - selectedAgentPolicies.filter((agentPolicy) => agentPolicy !== policyId) - ); - }; + }, [setSearch, setDraftKuery, setSelectedAgentPolicies, setSelectedStatus, setShowUpgradeable]); // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); @@ -221,65 +186,140 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const [agentToUnenroll, setAgentToUnenroll] = useState(undefined); const [agentToUpgrade, setAgentToUpgrade] = useState(undefined); - let kuery = search.trim(); - if (selectedAgentPolicies.length) { - if (kuery) { - kuery = `(${kuery}) and`; + // Kuery + const kuery = useMemo(() => { + let kueryBuilder = search.trim(); + if (selectedAgentPolicies.length) { + if (kueryBuilder) { + kueryBuilder = `(${kueryBuilder}) and`; + } + kueryBuilder = `${kueryBuilder} ${AGENT_SAVED_OBJECT_TYPE}.policy_id : (${selectedAgentPolicies + .map((agentPolicy) => `"${agentPolicy}"`) + .join(' or ')})`; } - kuery = `${kuery} ${AGENT_SAVED_OBJECT_TYPE}.policy_id : (${selectedAgentPolicies - .map((agentPolicy) => `"${agentPolicy}"`) - .join(' or ')})`; - } - if (selectedStatus.length) { - const kueryStatus = selectedStatus - .map((status) => { - switch (status) { - case 'online': - return AgentStatusKueryHelper.buildKueryForOnlineAgents(); - case 'offline': - return AgentStatusKueryHelper.buildKueryForOfflineAgents(); - case 'updating': - return AgentStatusKueryHelper.buildKueryForUpdatingAgents(); - case 'error': - return AgentStatusKueryHelper.buildKueryForErrorAgents(); - } + if (selectedStatus.length) { + const kueryStatus = selectedStatus + .map((status) => { + switch (status) { + case 'healthy': + return AgentStatusKueryHelper.buildKueryForOnlineAgents(); + case 'unhealthy': + return AgentStatusKueryHelper.buildKueryForErrorAgents(); + case 'offline': + return AgentStatusKueryHelper.buildKueryForOfflineAgents(); + case 'updating': + return AgentStatusKueryHelper.buildKueryForUpdatingAgents(); + case 'inactive': + return AgentStatusKueryHelper.buildKueryForInactiveAgents(); + } - return ''; - }) - .join(' or '); + return undefined; + }) + .filter((statusKuery) => statusKuery !== undefined) + .join(' or '); - if (kuery) { - kuery = `(${kuery}) and ${kueryStatus}`; - } else { - kuery = kueryStatus; + if (kueryBuilder) { + kueryBuilder = `(${kueryBuilder}) and ${kueryStatus}`; + } else { + kueryBuilder = kueryStatus; + } } - } - const agentsRequest = useGetAgents( - { - page: pagination.currentPage, - perPage: pagination.pageSize, - kuery: kuery && kuery !== '' ? kuery : undefined, - showInactive, - showUpgradeable, - }, - { - pollIntervalMs: REFRESH_INTERVAL_MS, + return kueryBuilder; + }, [selectedStatus, selectedAgentPolicies, search]); + + const showInactive = useMemo(() => { + return selectedStatus.includes('inactive'); + }, [selectedStatus]); + + const [agents, setAgents] = useState([]); + const [agentsStatus, setAgentsStatus] = useState< + { [key in SimplifiedAgentStatus]: number } | undefined + >(); + const [isLoading, setIsLoading] = useState(false); + const [totalAgents, setTotalAgents] = useState(0); + const [totalInactiveAgents, setTotalInactiveAgents] = useState(0); + + // Request to fetch agents and agent status + const currentRequestRef = useRef(0); + const fetchData = useCallback(() => { + async function fetchDataAsync() { + currentRequestRef.current++; + const currentRequest = currentRequestRef.current; + + try { + setIsLoading(true); + const [agentsRequest, agentsStatusRequest] = await Promise.all([ + sendGetAgents({ + page: pagination.currentPage, + perPage: pagination.pageSize, + kuery: kuery && kuery !== '' ? kuery : undefined, + showInactive, + showUpgradeable, + }), + sendGetAgentStatus({ + kuery: kuery && kuery !== '' ? kuery : undefined, + }), + ]); + // Return if a newer request as been triggered + if (currentRequestRef.current !== currentRequest) { + return; + } + if (agentsRequest.error) { + throw agentsRequest.error; + } + if (!agentsRequest.data) { + throw new Error('Invalid GET /agents response'); + } + if (agentsStatusRequest.error) { + throw agentsStatusRequest.error; + } + if (!agentsStatusRequest.data) { + throw new Error('Invalid GET /agents-status response'); + } + + setAgentsStatus({ + healthy: agentsStatusRequest.data.results.online, + unhealthy: agentsStatusRequest.data.results.error, + offline: agentsStatusRequest.data.results.offline, + updating: agentsStatusRequest.data.results.other, + inactive: agentsRequest.data.totalInactive, + }); + + setAgents(agentsRequest.data.list); + setTotalAgents(agentsRequest.data.total); + setTotalInactiveAgents(agentsRequest.data.totalInactive); + } catch (error) { + notifications.toasts.addError(error, { + title: i18n.translate('xpack.fleet.agentList.errorFetchingDataTitle', { + defaultMessage: 'Error fetching agents', + }), + }); + } + setIsLoading(false); } - ); + fetchDataAsync(); + }, [pagination, kuery, showInactive, showUpgradeable, notifications.toasts]); - const agents = agentsRequest.data ? agentsRequest.data.list : []; - const totalAgents = agentsRequest.data ? agentsRequest.data.total : 0; - const totalInactiveAgents = agentsRequest.data ? agentsRequest.data.totalInactive : 0; - const { isLoading } = agentsRequest; + // Send request to get agent list and status + useEffect(() => { + fetchData(); + const interval = setInterval(() => { + fetchData(); + }, REFRESH_INTERVAL_MS); + + return () => clearInterval(interval); + }, [fetchData]); const agentPoliciesRequest = useGetAgentPolicies({ page: 1, perPage: 1000, }); - // eslint-disable-next-line react-hooks/exhaustive-deps - const agentPolicies = agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []; + const agentPolicies = useMemo( + () => (agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []), + [agentPoliciesRequest] + ); const agentPoliciesIndexedById = useMemo(() => { return agentPolicies.reduce((acc, agentPolicy) => { acc[agentPolicy.id] = agentPolicy; @@ -287,7 +327,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { return acc; }, {} as { [k: string]: AgentPolicy }); }, [agentPolicies]); - const { isLoading: isAgentPoliciesLoading } = agentPoliciesRequest; const columns = [ { @@ -405,7 +444,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { return ( agentsRequest.resendRequest()} + refresh={() => fetchData()} onReassignClick={() => setAgentToReassign(agent)} onUnenrollClick={() => setAgentToUnenroll(agent)} onUpgradeClick={() => setAgentToUpgrade(agent)} @@ -452,7 +491,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { agents={[agentToReassign]} onClose={() => { setAgentToReassign(undefined); - agentsRequest.resendRequest(); + fetchData(); }} /> @@ -464,7 +503,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { agentCount={1} onClose={() => { setAgentToUnenroll(undefined); - agentsRequest.resendRequest(); + fetchData(); }} useForceUnenroll={agentToUnenroll.status === 'unenrolling'} /> @@ -478,7 +517,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { agentCount={1} onClose={() => { setAgentToUpgrade(undefined); - agentsRequest.resendRequest(); + fetchData(); }} version={kibanaVersion} /> @@ -486,134 +525,26 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { )} {/* Search and filter bar */} - - - - - { - setPagination({ - ...pagination, - currentPage: 1, - }); - setSearch(newSearch); - }} - fieldPrefix={AGENT_SAVED_OBJECT_TYPE} - /> - - - - setIsStatutsFilterOpen(!isStatusFilterOpen)} - isSelected={isStatusFilterOpen} - hasActiveFilters={selectedStatus.length > 0} - numActiveFilters={selectedStatus.length} - disabled={isAgentPoliciesLoading} - > - - - } - isOpen={isStatusFilterOpen} - closePopover={() => setIsStatutsFilterOpen(false)} - panelPaddingSize="none" - > -
- {statusFilters.map(({ label, status }, idx) => ( - { - if (selectedStatus.includes(status)) { - setSelectedStatus([...selectedStatus.filter((s) => s !== status)]); - } else { - setSelectedStatus([...selectedStatus, status]); - } - }} - > - {label} - - ))} -
-
- setIsAgentPoliciesFilterOpen(!isAgentPoliciesFilterOpen)} - isSelected={isAgentPoliciesFilterOpen} - hasActiveFilters={selectedAgentPolicies.length > 0} - numActiveFilters={selectedAgentPolicies.length} - numFilters={agentPolicies.length} - disabled={isAgentPoliciesLoading} - > - - - } - isOpen={isAgentPoliciesFilterOpen} - closePopover={() => setIsAgentPoliciesFilterOpen(false)} - panelPaddingSize="none" - > -
- {agentPolicies.map((agentPolicy, index) => ( - { - if (selectedAgentPolicies.includes(agentPolicy.id)) { - removeAgentPolicyFilter(agentPolicy.id); - } else { - addAgentPolicyFilter(agentPolicy.id); - } - }} - > - {agentPolicy.name} - - ))} -
-
- { - setShowUpgradeable(!showUpgradeable); - }} - > - - - setShowInactive(!showInactive)} - > - - -
-
-
-
-
+ - {/* Agent total and bulk actions */} - agent.active).length || 0} selectionMode={selectionMode} setSelectionMode={setSelectionMode} @@ -625,10 +556,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { setSelectionMode('manual'); } }} - refreshAgents={() => agentsRequest.resendRequest()} + refreshAgents={() => fetchData()} /> - - + {/* Agent list table */} @@ -638,7 +568,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { loading={isLoading} hasActions={true} noItemsMessage={ - isLoading && agentsRequest.isInitialRequest ? ( + isLoading && currentRequestRef.current === 1 ? ( - + ), Unhealthy: ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx index bf0163fe904e6..dfa093ca8bf80 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx @@ -5,122 +5,31 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; -import { - EuiHealth, - EuiText, - EuiFlexGroup, - EuiFlexItem, - EuiStat, - EuiI18nNumber, - EuiButton, -} from '@elastic/eui'; +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiButton, EuiPortal } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { useRouteMatch } from 'react-router-dom'; import { PAGE_ROUTING_PATHS } from '../../../constants'; import { WithHeaderLayout } from '../../../layouts'; import { useCapabilities, useLink, useGetAgentPolicies } from '../../../hooks'; -import { useGetAgentStatus } from '../../agent_policy/details_page/hooks'; import { AgentEnrollmentFlyout } from '../components'; -import { DonutChart } from './donut_chart'; - -const REFRESH_INTERVAL_MS = 5000; - -const Divider = styled.div` - width: 0; - height: 100%; - border-left: ${(props) => props.theme.eui.euiBorderThin}; - height: 45px; -`; export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; - const agentStatusRequest = useGetAgentStatus(undefined, { - pollIntervalMs: REFRESH_INTERVAL_MS, - }); - const agentStatus = agentStatusRequest.data?.results; // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = React.useState(false); - const headerRightColumn = ( - - - } - description={i18n.translate('xpack.fleet.agentListStatus.totalLabel', { - defaultMessage: 'Agents', - })} - /> - - - - + const headerRightColumn = hasWriteCapabilites ? ( + - - - - } - description={i18n.translate('xpack.fleet.agentListStatus.onlineLabel', { - defaultMessage: 'Online', - })} - /> + setIsEnrollmentFlyoutOpen(true)}> + + - - } - description={i18n.translate('xpack.fleet.agentListStatus.offlineLabel', { - defaultMessage: 'Offline', - })} - /> - - - } - description={i18n.translate('xpack.fleet.agentListStatus.errorLabel', { - defaultMessage: 'Error', - })} - /> - - {hasWriteCapabilites && ( - <> - - - - - setIsEnrollmentFlyoutOpen(true)}> - - - - - )} - ); + ) : undefined; const headerLeftColumn = ( @@ -177,10 +86,12 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { } > {isEnrollmentFlyoutOpen ? ( - setIsEnrollmentFlyoutOpen(false)} - /> + + setIsEnrollmentFlyoutOpen(false)} + /> + ) : null} {children} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx new file mode 100644 index 0000000000000..5e7b42798c294 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { euiPaletteColorBlindBehindText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SimplifiedAgentStatus } from '../../../types'; + +const visColors = euiPaletteColorBlindBehindText(); +const colorToHexMap = { + // TODO - replace with variable once https://github.com/elastic/eui/issues/2731 is closed + default: '#d3dae6', + primary: visColors[1], + secondary: visColors[0], + accent: visColors[2], + warning: visColors[5], + danger: visColors[9], +}; + +export const AGENT_STATUSES: SimplifiedAgentStatus[] = [ + 'healthy', + 'unhealthy', + 'updating', + 'offline', + 'inactive', +]; + +export function getColorForAgentStatus(agentStatus: SimplifiedAgentStatus): string { + switch (agentStatus) { + case 'healthy': + return colorToHexMap.secondary; + case 'offline': + case 'inactive': + return colorToHexMap.default; + case 'unhealthy': + return colorToHexMap.warning; + case 'updating': + return colorToHexMap.primary; + default: + throw new Error(`Insuported Agent status ${agentStatus}`); + } +} + +export function getLabelForAgentStatus(agentStatus: SimplifiedAgentStatus): string { + switch (agentStatus) { + case 'healthy': + return i18n.translate('xpack.fleet.agentStatus.healthyLabel', { + defaultMessage: 'Healthy', + }); + case 'offline': + return i18n.translate('xpack.fleet.agentStatus.offlineLabel', { + defaultMessage: 'Offline', + }); + case 'inactive': + return i18n.translate('xpack.fleet.agentStatus.inactiveLabel', { + defaultMessage: 'Inactive', + }); + case 'unhealthy': + return i18n.translate('xpack.fleet.agentStatus.unhealthyLabel', { + defaultMessage: 'Unhealthy', + }); + case 'updating': + return i18n.translate('xpack.fleet.agentStatus.updatingLabel', { + defaultMessage: 'Updating', + }); + default: + throw new Error(`Insuported Agent status ${agentStatus}`); + } +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx index b96fda2c23af1..42e4a6051d725 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx @@ -21,6 +21,7 @@ import { Loading } from '../../../components'; import { PackageList } from '../../../types'; import { useLocalSearch, searchIdField } from '../hooks'; import { PackageCard } from './package_card'; +import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; interface ListProps { isLoading?: boolean; @@ -118,7 +119,7 @@ function GridColumn({ list }: GridColumnProps) { {list.length ? ( list.map((item) => ( - + )) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/index.tsx index 8884d1f9d7a75..733aa9dfcf8aa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/index.tsx @@ -11,6 +11,7 @@ import { useBreadcrumbs } from '../../hooks'; import { CreatePackagePolicyPage } from '../agent_policy/create_package_policy_page'; import { EPMHomePage } from './screens/home'; import { Detail } from './screens/detail'; +import { Policy } from './screens/policy'; export const EPMApp: React.FunctionComponent = () => { useBreadcrumbs('integrations'); @@ -20,6 +21,9 @@ export const EPMApp: React.FunctionComponent = () => { + + + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index 7ed14a27c32cf..3d43725f2dc71 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -10,17 +10,26 @@ import React, { lazy, memo } from 'react'; import { PAGE_ROUTING_PATHS, pagePathGetters } from '../../../../constants'; import { Route } from 'react-router-dom'; import { + GetAgentPoliciesResponse, GetFleetStatusResponse, GetInfoResponse, + GetPackagePoliciesResponse, } from '../../../../../../../common/types/rest_spec'; import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models'; -import { epmRouteService, fleetSetupRouteService } from '../../../../../../../common/services'; -import { act } from '@testing-library/react'; +import { + agentPolicyRouteService, + epmRouteService, + fleetSetupRouteService, + packagePolicyRouteService, +} from '../../../../../../../common/services'; +import { act, cleanup } from '@testing-library/react'; describe('when on integration detail', () => { - const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7' }); + const pkgkey = 'nginx-0.3.7'; + const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey }); let testRenderer: TestRenderer; let renderResult: ReturnType; + let mockedApi: MockedApi; const render = () => (renderResult = testRenderer.render( @@ -30,10 +39,15 @@ describe('when on integration detail', () => { beforeEach(() => { testRenderer = createTestRendererMock(); - mockApiCalls(testRenderer.startServices.http); + mockedApi = mockApiCalls(testRenderer.startServices.http); testRenderer.history.push(detailPageUrlPath); }); + afterEach(() => { + cleanup(); + window.location.hash = '#/'; + }); + describe('and a custom UI extension is NOT registered', () => { beforeEach(() => render()); @@ -137,9 +151,48 @@ describe('when on integration detail', () => { }); }); }); + + describe('and on the Policies Tab', () => { + const policiesTabURLPath = pagePathGetters.integration_details({ pkgkey, panel: 'policies' }); + beforeEach(() => { + testRenderer.history.push(policiesTabURLPath); + render(); + }); + + it('should display policies list', () => { + const table = renderResult.getByTestId('integrationPolicyTable'); + expect(table).not.toBeNull(); + }); + + it('should link to integration policy detail when an integration policy is clicked', async () => { + await mockedApi.waitForApi(); + const firstPolicy = renderResult.getByTestId('integrationNameLink') as HTMLAnchorElement; + expect(firstPolicy.href).toEqual( + 'http://localhost/mock/app/fleet#/integrations/edit-integration/e8a37031-2907-44f6-89d2-98bd493f60dc' + ); + }); + }); }); -const mockApiCalls = (http: MockedFleetStartServices['http']) => { +interface MockedApi { + /** Will return a promise that resolves when triggered APIs are complete */ + waitForApi: () => Promise; +} + +const mockApiCalls = (http: MockedFleetStartServices['http']): MockedApi => { + let inflightApiCalls = 0; + const apiDoneListeners: Array<() => void> = []; + const markApiCallAsHandled = async () => { + inflightApiCalls++; + await new Promise((r) => setTimeout(r, 1)); + inflightApiCalls--; + + // If no more pending API calls, then notify listeners + if (inflightApiCalls === 0 && apiDoneListeners.length > 0) { + apiDoneListeners.splice(0).forEach((listener) => listener()); + } + }; + // @ts-ignore const epmPackageResponse: GetInfoResponse = { response: { @@ -369,7 +422,7 @@ const mockApiCalls = (http: MockedFleetStartServices['http']) => { owner: { github: 'elastic/integrations-services' }, latestVersion: '0.3.7', removable: true, - status: 'not_installed', + status: 'installed', }, } as GetInfoResponse; @@ -388,24 +441,162 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos const agentsSetupResponse: GetFleetStatusResponse = { isReady: true, missing_requirements: [] }; + const packagePoliciesResponse: GetPackagePoliciesResponse = { + items: [ + { + id: 'e8a37031-2907-44f6-89d2-98bd493f60dc', + version: 'WzgzMiwxXQ==', + name: 'nginx-1', + description: '', + namespace: 'default', + policy_id: '521c1b70-3976-11eb-ad1c-3baa423084d9', + enabled: true, + output_id: '', + inputs: [ + { + type: 'logfile', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { paths: { value: ['/var/log/nginx/access.log*'], type: 'text' } }, + id: 'logfile-nginx.access-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + paths: ['/var/log/nginx/access.log*'], + exclude_files: ['.gz$'], + processors: [{ add_locale: null }], + }, + }, + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.error' }, + vars: { paths: { value: ['/var/log/nginx/error.log*'], type: 'text' } }, + id: 'logfile-nginx.error-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + paths: ['/var/log/nginx/error.log*'], + exclude_files: ['.gz$'], + multiline: { + pattern: '^\\d{4}\\/\\d{2}\\/\\d{2} ', + negate: true, + match: 'after', + }, + processors: [{ add_locale: null }], + }, + }, + { + enabled: false, + data_stream: { type: 'logs', dataset: 'nginx.ingress_controller' }, + vars: { paths: { value: ['/var/log/nginx/ingress.log*'], type: 'text' } }, + id: 'logfile-nginx.ingress_controller-e8a37031-2907-44f6-89d2-98bd493f60dc', + }, + ], + }, + { + type: 'nginx/metrics', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'nginx.stubstatus' }, + vars: { + period: { value: '10s', type: 'text' }, + server_status_path: { value: '/nginx_status', type: 'text' }, + }, + id: 'nginx/metrics-nginx.stubstatus-e8a37031-2907-44f6-89d2-98bd493f60dc', + compiled_stream: { + metricsets: ['stubstatus'], + hosts: ['http://127.0.0.1:80'], + period: '10s', + server_status_path: '/nginx_status', + }, + }, + ], + vars: { hosts: { value: ['http://127.0.0.1:80'], type: 'text' } }, + }, + ], + package: { name: 'nginx', title: 'Nginx', version: '0.3.7' }, + revision: 1, + created_at: '2020-12-09T13:46:31.013Z', + created_by: 'elastic', + updated_at: '2020-12-09T13:46:31.013Z', + updated_by: 'elastic', + }, + ], + total: 1, + page: 1, + perPage: 20, + }; + + const agentPoliciesResponse: GetAgentPoliciesResponse = { + items: [ + { + id: '521c1b70-3976-11eb-ad1c-3baa423084d9', + name: 'Default', + namespace: 'default', + description: 'Default agent policy created by Kibana', + status: 'active', + package_policies: [ + '4d09bd78-b0ad-4238-9fa3-d87d3c887c73', + '2babac18-eb8e-4ce4-b53b-4b7c5f507019', + 'e8a37031-2907-44f6-89d2-98bd493f60dc', + ], + is_default: true, + monitoring_enabled: ['logs', 'metrics'], + revision: 6, + updated_at: '2020-12-09T13:46:31.840Z', + updated_by: 'elastic', + agents: 0, + }, + ], + total: 1, + page: 1, + perPage: 100, + }; + http.get.mockImplementation(async (path) => { if (typeof path === 'string') { if (path === epmRouteService.getInfoPath(`nginx-0.3.7`)) { + markApiCallAsHandled(); return epmPackageResponse; } if (path === epmRouteService.getFilePath('/package/nginx/0.3.7/docs/README.md')) { + markApiCallAsHandled(); return packageReadMe; } if (path === fleetSetupRouteService.getFleetSetupPath()) { + markApiCallAsHandled(); return agentsSetupResponse; } + if (path === packagePolicyRouteService.getListPath()) { + markApiCallAsHandled(); + return packagePoliciesResponse; + } + + if (path === agentPolicyRouteService.getListPath()) { + markApiCallAsHandled(); + return agentPoliciesResponse; + } + const err = new Error(`API [GET ${path}] is not MOCKED!`); // eslint-disable-next-line no-console - console.log(err); + console.error(err); throw err; } }); + + return { + waitForApi() { + return new Promise((resolve) => { + if (inflightApiCalls > 0) { + apiDoneListeners.push(resolve); + } else { + resolve(); + } + }); + }, + }; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index 467276f0d0b8c..c70a11db004a6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -43,6 +43,7 @@ import { Content } from './content'; import './index.scss'; import { useUIExtension } from '../../../../hooks/use_ui_extension'; import { PLUGIN_ID } from '../../../../../../../common/constants'; +import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; export const DEFAULT_PANEL: DetailViewPanelName = 'overview'; @@ -315,7 +316,7 @@ export function Detail() { isSelected: panelId === panel, 'data-test-subj': `tab-${panelId}`, href: getHref('integration_details', { - pkgkey: `${packageInfo?.name}-${packageInfo?.version}`, + pkgkey: pkgKeyFromPackageInfo(packageInfo || {}), panel: panelId, }), }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx index 8609b08c9a774..4061b86f1f740 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx @@ -36,8 +36,8 @@ const IntegrationDetailsLink = memo<{ return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/policy/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/policy/index.tsx new file mode 100644 index 0000000000000..fcd4821996efe --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/policy/index.tsx @@ -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 React, { memo } from 'react'; +import { useRouteMatch } from 'react-router-dom'; +import { EditPackagePolicyForm } from '../../../agent_policy/edit_package_policy_page'; + +export const Policy = memo(() => { + const { + params: { packagePolicyId }, + } = useRouteMatch<{ packagePolicyId: string }>(); + + return ; +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/services/pkg_key_from_package_info.ts b/x-pack/plugins/fleet/public/applications/fleet/services/pkg_key_from_package_info.ts new file mode 100644 index 0000000000000..0e38abe6f5160 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/services/pkg_key_from_package_info.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const pkgKeyFromPackageInfo = ( + packageInfo: T +): string => { + return `${packageInfo.name}-${packageInfo.version}`; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index dd80c1ad77b85..dadacf6006085 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -12,6 +12,7 @@ export { AgentPolicy, NewAgentPolicy, AgentEvent, + SimplifiedAgentStatus, EnrollmentAPIKey, PackagePolicy, NewPackagePolicy, diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index 08372571240ff..222554e97eb91 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -12,6 +12,7 @@ import { KibanaResponseFactory, } from 'src/core/server'; import { errors as LegacyESErrors } from 'elasticsearch'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { appContextService } from '../services'; import { IngestManagerError, @@ -51,6 +52,10 @@ export const isLegacyESClientError = (error: any): error is LegacyESClientError return error instanceof LegacyESErrors._Abstract; }; +export function isESClientError(error: unknown): error is ResponseError { + return error instanceof ResponseError; +} + const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof RegistryError) { return 502; // Bad Gateway diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index d6fa79a2baeba..fad4eef66215d 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -9,6 +9,7 @@ export { defaultIngestErrorHandler, ingestErrorToResponseOptions, isLegacyESClientError, + isESClientError, } from './handlers'; export class IngestManagerError extends Error { diff --git a/x-pack/plugins/fleet/server/mocks.ts b/x-pack/plugins/fleet/server/mocks.ts index bc3e89ef6d3ce..9e2c71ead5b74 100644 --- a/x-pack/plugins/fleet/server/mocks.ts +++ b/x-pack/plugins/fleet/server/mocks.ts @@ -15,7 +15,7 @@ export const createAppContextStartContractMock = (): FleetAppContext => { return { encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), savedObjects: savedObjectsServiceMock.createStartContract(), - security: securityMock.createSetup(), + security: securityMock.createStart(), logger: loggingSystemMock.create().get(), isProductionMode: true, kibanaVersion: '8.0.0', diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 7ddd6e3ba3fe0..0b58c4aab9d0b 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -24,7 +24,7 @@ import { EncryptedSavedObjectsPluginStart, EncryptedSavedObjectsPluginSetup, } from '../../encrypted_saved_objects/server'; -import { SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { PLUGIN_ID, @@ -85,12 +85,15 @@ export interface FleetSetupDeps { usageCollection?: UsageCollectionSetup; } -export type FleetStartDeps = object; +export interface FleetStartDeps { + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + security?: SecurityPluginStart; +} export interface FleetAppContext { encryptedSavedObjectsStart: EncryptedSavedObjectsPluginStart; encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; - security?: SecurityPluginSetup; + security?: SecurityPluginStart; config$?: Observable; savedObjects: SavedObjectsServiceStart; isProductionMode: PluginInitializerContext['env']['mode']['prod']; @@ -150,7 +153,6 @@ export class FleetPlugin implements Plugin { private licensing$!: Observable; private config$: Observable; - private security: SecurityPluginSetup | undefined; private cloud: CloudSetup | undefined; private logger: Logger | undefined; @@ -171,9 +173,6 @@ export class FleetPlugin public async setup(core: CoreSetup, deps: FleetSetupDeps) { this.httpSetup = core.http; this.licensing$ = deps.licensing.license$; - if (deps.security) { - this.security = deps.security; - } this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects; this.cloud = deps.cloud; @@ -226,7 +225,7 @@ export class FleetPlugin // For all the routes we enforce the user to have role superuser const routerSuperuserOnly = makeRouterEnforcingSuperuser(router); // Register rest of routes only if security is enabled - if (this.security) { + if (deps.security) { registerSetupRoutes(routerSuperuserOnly, config); registerAgentPolicyRoutes(routerSuperuserOnly); registerPackagePolicyRoutes(routerSuperuserOnly); @@ -262,16 +261,11 @@ export class FleetPlugin } } - public async start( - core: CoreStart, - plugins: { - encryptedSavedObjects: EncryptedSavedObjectsPluginStart; - } - ): Promise { + public async start(core: CoreStart, plugins: FleetStartDeps): Promise { await appContextService.start({ encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, - security: this.security, + security: plugins.security, config$: this.config$, savedObjects: core.savedObjects, isProductionMode: this.isProductionMode, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index eff7d3c3c5cf3..a867196f9762f 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -330,7 +330,8 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< // TODO change path const results = await AgentService.getAgentStatusForAgentPolicy( soClient, - request.query.policyId + request.query.policyId, + request.query.kuery ); const body: GetAgentStatusResponse = { results }; diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index b2ad9591bc2ee..f87cf8026c560 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -15,7 +15,9 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re const soClient = context.core.savedObjects.client; try { const isAdminUserSetup = (await outputService.getAdminUser(soClient)) !== null; - const isApiKeysEnabled = await appContextService.getSecurity().authc.areAPIKeysEnabled(); + const isApiKeysEnabled = await appContextService + .getSecurity() + .authc.apiKeys.areAPIKeysEnabled(); const isTLSEnabled = appContextService.getHttpSetup().getServerInfo().protocol === 'https'; const isProductionMode = appContextService.getIsProductionMode(); const isCloud = appContextService.getCloud()?.isCloudEnabled ?? false; diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 35033cbe86ea5..0dfa6db7df9be 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -23,7 +23,8 @@ export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus; export async function getAgentStatusForAgentPolicy( soClient: SavedObjectsClientContract, - agentPolicyId?: string + agentPolicyId?: string, + filterKuery?: string ) { const [all, online, error, offline] = await Promise.all( [ @@ -36,15 +37,29 @@ export async function getAgentStatusForAgentPolicy( showInactive: false, perPage: 0, page: 1, - kuery: agentPolicyId - ? kuery - ? `(${kuery}) and (${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${agentPolicyId}")` - : `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${agentPolicyId}"` - : kuery, + kuery: joinKuerys( + ...[ + kuery, + filterKuery, + agentPolicyId ? `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${agentPolicyId}"` : undefined, + ] + ), }) ) ); + function joinKuerys(...kuerys: Array) { + return kuerys + .filter((kuery) => kuery !== undefined) + .reduce((acc, kuery) => { + if (acc === '') { + return `(${kuery})`; + } + + return `${acc} and (${kuery})`; + }, ''); + } + return { events: await getEventsCount(soClient, agentPolicyId), total: all.total, diff --git a/x-pack/plugins/fleet/server/services/api_keys/security.ts b/x-pack/plugins/fleet/server/services/api_keys/security.ts index 5fdf8626a9fb2..9a32da3cff46f 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/security.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/security.ts @@ -6,7 +6,7 @@ import type { Request } from '@hapi/hapi'; import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server'; -import { FleetAdminUserInvalidError, isLegacyESClientError } from '../../errors'; +import { FleetAdminUserInvalidError, isESClientError } from '../../errors'; import { CallESAsCurrentUser } from '../../types'; import { appContextService } from '../app_context'; import { outputService } from '../output'; @@ -37,14 +37,14 @@ export async function createAPIKey( } try { - const key = await security.authc.createAPIKey(request, { + const key = await security.authc.apiKeys.create(request, { name, role_descriptors: roleDescriptors, }); return key; } catch (err) { - if (isLegacyESClientError(err) && err.statusCode === 401) { + if (isESClientError(err) && err.statusCode === 401) { // Clear Fleet admin user cache as the user is probably not valid anymore outputService.invalidateCache(); throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`); @@ -87,13 +87,13 @@ export async function invalidateAPIKey(soClient: SavedObjectsClientContract, id: } try { - const res = await security.authc.invalidateAPIKey(request, { + const res = await security.authc.apiKeys.invalidate(request, { id, }); return res; } catch (err) { - if (isLegacyESClientError(err) && err.statusCode === 401) { + if (isESClientError(err) && err.statusCode === 401) { // Clear Fleet admin user cache as the user is probably not valid anymore outputService.invalidateCache(); throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`); diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 5c4e33d50b480..bcf056c9482cb 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -11,7 +11,7 @@ import { EncryptedSavedObjectsPluginSetup, } from '../../../encrypted_saved_objects/server'; import packageJSON from '../../../../../package.json'; -import { SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginStart } from '../../../security/server'; import { FleetConfigType } from '../../common'; import { ExternalCallback, ExternalCallbacksStorage, FleetAppContext } from '../plugin'; import { CloudSetup } from '../../../cloud/server'; @@ -19,7 +19,7 @@ import { CloudSetup } from '../../../cloud/server'; class AppContextService { private encryptedSavedObjects: EncryptedSavedObjectsClient | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; - private security: SecurityPluginSetup | undefined; + private security: SecurityPluginStart | undefined; private config$?: Observable; private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 6de94cd9c936d..3e9262c2a9124 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -246,5 +246,6 @@ export const UpdateAgentRequestSchema = { export const GetAgentStatusRequestSchema = { query: schema.object({ policyId: schema.maybe(schema.string()), + kuery: schema.maybe(schema.string()), }), }; diff --git a/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts b/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts index e9f116b79f990..815f5ad3ecafe 100644 --- a/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts +++ b/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GraphWorkspaceSavedObject, Workspace } from '../../types'; -import { savedWorkspaceToAppState } from './deserialize'; +import { GraphWorkspaceSavedObject, IndexPatternSavedObject, Workspace } from '../../types'; +import { migrateLegacyIndexPatternRef, savedWorkspaceToAppState } from './deserialize'; import { createWorkspace } from '../../angular/graph_client_workspace'; import { outlinkEncoders } from '../../helpers/outlink_encoders'; import { IndexPattern } from '../../../../../../src/plugins/data/public'; @@ -21,7 +21,7 @@ describe('deserialize', () => { numLinks: 2, numVertices: 4, wsState: JSON.stringify({ - indexPattern: 'Testindexpattern', + indexPattern: '123', selectedFields: [ { color: 'black', name: 'field1', selected: true, iconClass: 'a' }, { color: 'black', name: 'field2', selected: true, iconClass: 'b' }, @@ -208,4 +208,32 @@ describe('deserialize', () => { expect(workspace.edges[1].source).toBe(workspace.nodes[2]); expect(workspace.edges[1].target).toBe(workspace.nodes[4]); }); + + describe('migrateLegacyIndexPatternRef', () => { + it('should migrate legacy index pattern ref', () => { + const workspacePayload = { ...savedWorkspace, legacyIndexPatternRef: 'Testpattern' }; + const success = migrateLegacyIndexPatternRef(workspacePayload, [ + { id: '678', attributes: { title: 'Testpattern' } } as IndexPatternSavedObject, + { id: '123', attributes: { title: 'otherpattern' } } as IndexPatternSavedObject, + ]); + expect(success).toEqual({ success: true }); + expect(workspacePayload.legacyIndexPatternRef).toBeUndefined(); + expect(JSON.parse(workspacePayload.wsState).indexPattern).toBe('678'); + }); + + it('should return false if migration fails', () => { + const workspacePayload = { ...savedWorkspace, legacyIndexPatternRef: 'Testpattern' }; + const success = migrateLegacyIndexPatternRef(workspacePayload, [ + { id: '123', attributes: { title: 'otherpattern' } } as IndexPatternSavedObject, + ]); + expect(success).toEqual({ success: false, missingIndexPattern: 'Testpattern' }); + }); + + it('should not modify migrated workspaces', () => { + const workspacePayload = { ...savedWorkspace }; + const success = migrateLegacyIndexPatternRef(workspacePayload, []); + expect(success).toEqual({ success: true }); + expect(workspacePayload).toEqual(savedWorkspace); + }); + }); }); diff --git a/x-pack/plugins/graph/public/services/persistence/deserialize.ts b/x-pack/plugins/graph/public/services/persistence/deserialize.ts index 324bf10cdd99c..a25e4b7e5e3dc 100644 --- a/x-pack/plugins/graph/public/services/persistence/deserialize.ts +++ b/x-pack/plugins/graph/public/services/persistence/deserialize.ts @@ -61,19 +61,39 @@ function deserializeUrlTemplate({ return template; } -// returns the id of the index pattern, lookup is done in app.js -export function lookupIndexPattern( +/** + * Migrates `savedWorkspace` to use the id instead of the title of the referenced index pattern. + * Returns a status indicating successful migration or failure to look up the index pattern by title. + * If the workspace is migrated already, a success status is returned as well. + * @param savedWorkspace The workspace saved object to migrate. The migration will happen in-place and mutate the passed in object + * @param indexPatterns All index patterns existing in the current space + */ +export function migrateLegacyIndexPatternRef( savedWorkspace: GraphWorkspaceSavedObject, indexPatterns: IndexPatternSavedObject[] -) { +): { success: true } | { success: false; missingIndexPattern: string } { + const legacyIndexPatternRef = savedWorkspace.legacyIndexPatternRef; + if (!legacyIndexPatternRef) { + return { success: true }; + } + const indexPatternId = indexPatterns.find( + (pattern) => pattern.attributes.title === legacyIndexPatternRef + )?.id; + if (!indexPatternId) { + return { success: false, missingIndexPattern: legacyIndexPatternRef }; + } const serializedWorkspaceState: SerializedWorkspaceState = JSON.parse(savedWorkspace.wsState); - const indexPattern = indexPatterns.find( - (pattern) => pattern.attributes.title === serializedWorkspaceState.indexPattern - ); + serializedWorkspaceState.indexPattern = indexPatternId!; + savedWorkspace.wsState = JSON.stringify(serializedWorkspaceState); + delete savedWorkspace.legacyIndexPatternRef; + return { success: true }; +} - if (indexPattern) { - return indexPattern; - } +// returns the id of the index pattern, lookup is done in app.js +export function lookupIndexPatternId(savedWorkspace: GraphWorkspaceSavedObject) { + const serializedWorkspaceState: SerializedWorkspaceState = JSON.parse(savedWorkspace.wsState); + + return serializedWorkspaceState.indexPattern; } // returns all graph fields mapped out of the index pattern diff --git a/x-pack/plugins/graph/public/services/persistence/serialize.test.ts b/x-pack/plugins/graph/public/services/persistence/serialize.test.ts index 0c9de0418a738..f142db9db0f7a 100644 --- a/x-pack/plugins/graph/public/services/persistence/serialize.test.ts +++ b/x-pack/plugins/graph/public/services/persistence/serialize.test.ts @@ -184,7 +184,7 @@ describe('serialize', () => { "timeoutMillis": 5000, "useSignificance": true, }, - "indexPattern": "Testindexpattern", + "indexPattern": "123", "links": Array [ Object { "label": "", diff --git a/x-pack/plugins/graph/public/services/persistence/serialize.ts b/x-pack/plugins/graph/public/services/persistence/serialize.ts index a3a76a8a08eba..9ed73de3a620d 100644 --- a/x-pack/plugins/graph/public/services/persistence/serialize.ts +++ b/x-pack/plugins/graph/public/services/persistence/serialize.ts @@ -109,7 +109,7 @@ export function appStateToSavedWorkspace( const mappedUrlTemplates = urlTemplates.map(serializeUrlTemplate); const persistedWorkspaceState: SerializedWorkspaceState = { - indexPattern: selectedIndex.title, + indexPattern: selectedIndex.id, selectedFields: selectedFields.map(serializeField), blocklist, vertices, diff --git a/x-pack/plugins/graph/public/state_management/datasource.sagas.ts b/x-pack/plugins/graph/public/state_management/datasource.sagas.ts index f468ce5beb21c..4bf897acce757 100644 --- a/x-pack/plugins/graph/public/state_management/datasource.sagas.ts +++ b/x-pack/plugins/graph/public/state_management/datasource.sagas.ts @@ -44,7 +44,10 @@ export const datasourceSaga = ({ yield put(setDatasource({ type: 'none' })); notifications.toasts.addDanger( i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { - defaultMessage: 'Index pattern not found', + defaultMessage: 'Index pattern "{name}" not found', + values: { + name: action.payload.title, + }, }) ); } diff --git a/x-pack/plugins/graph/public/state_management/mocks.ts b/x-pack/plugins/graph/public/state_management/mocks.ts index f28f51544a6ed..f9245881b32e1 100644 --- a/x-pack/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/plugins/graph/public/state_management/mocks.ts @@ -61,7 +61,9 @@ export function createMockGraphStore({ getWorkspace: jest.fn(() => workspaceMock), getSavedWorkspace: jest.fn(() => savedWorkspace), indexPatternProvider: { - get: jest.fn(() => Promise.resolve(({} as unknown) as IndexPattern)), + get: jest.fn(() => + Promise.resolve(({ id: '123', title: 'test-pattern' } as unknown) as IndexPattern) + ), }, indexPatterns: [ ({ id: '123', attributes: { title: 'test-pattern' } } as unknown) as IndexPatternSavedObject, diff --git a/x-pack/plugins/graph/public/state_management/persistence.test.ts b/x-pack/plugins/graph/public/state_management/persistence.test.ts index efad5f95fd839..687488b0f396f 100644 --- a/x-pack/plugins/graph/public/state_management/persistence.test.ts +++ b/x-pack/plugins/graph/public/state_management/persistence.test.ts @@ -11,14 +11,15 @@ import { IndexpatternDatasource, datasourceSelector } from './datasource'; import { fieldsSelector } from './fields'; import { metaDataSelector, updateMetaData } from './meta_data'; import { templatesSelector } from './url_templates'; -import { lookupIndexPattern, appStateToSavedWorkspace } from '../services/persistence'; +import { migrateLegacyIndexPatternRef, appStateToSavedWorkspace } from '../services/persistence'; import { settingsSelector } from './advanced_settings'; import { openSaveModal } from '../services/save_modal'; const waitForPromise = () => new Promise((r) => setTimeout(r)); jest.mock('../services/persistence', () => ({ - lookupIndexPattern: jest.fn(() => ({ id: '123', attributes: { title: 'test-pattern' } })), + lookupIndexPatternId: jest.fn(() => ({ id: '123', attributes: { title: 'test-pattern' } })), + migrateLegacyIndexPatternRef: jest.fn(() => ({ success: true })), savedWorkspaceToAppState: jest.fn(() => ({ urlTemplates: [ { @@ -67,7 +68,7 @@ describe('persistence sagas', () => { }); it('should warn with a toast and abort if index pattern is not found', async () => { - (lookupIndexPattern as jest.Mock).mockReturnValueOnce(undefined); + (migrateLegacyIndexPatternRef as jest.Mock).mockReturnValueOnce({ success: false }); env.store.dispatch(loadSavedWorkspace({} as GraphWorkspaceSavedObject)); await waitForPromise(); expect(env.mockedDeps.notifications.toasts.addDanger).toHaveBeenCalled(); diff --git a/x-pack/plugins/graph/public/state_management/persistence.ts b/x-pack/plugins/graph/public/state_management/persistence.ts index cf6566f0c5f86..1d635a88714cc 100644 --- a/x-pack/plugins/graph/public/state_management/persistence.ts +++ b/x-pack/plugins/graph/public/state_management/persistence.ts @@ -15,9 +15,10 @@ import { loadFields, selectedFieldsSelector } from './fields'; import { updateSettings, settingsSelector } from './advanced_settings'; import { loadTemplates, templatesSelector } from './url_templates'; import { - lookupIndexPattern, + migrateLegacyIndexPatternRef, savedWorkspaceToAppState, appStateToSavedWorkspace, + lookupIndexPatternId, } from '../services/persistence'; import { updateMetaData, metaDataSelector } from './meta_data'; import { openSaveModal, SaveWorkspaceHandler } from '../services/save_modal'; @@ -43,23 +44,28 @@ export const loadingSaga = ({ indexPatternProvider, }: GraphStoreDependencies) => { function* deserializeWorkspace(action: Action) { - const selectedIndex = lookupIndexPattern(action.payload, indexPatterns); - if (!selectedIndex) { + const workspacePayload = action.payload; + const migrationStatus = migrateLegacyIndexPatternRef(workspacePayload, indexPatterns); + if (!migrationStatus.success) { notifications.toasts.addDanger( i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { - defaultMessage: 'Index pattern not found', + defaultMessage: 'Index pattern "{name}" not found', + values: { + name: migrationStatus.missingIndexPattern, + }, }) ); return; } - const indexPattern = yield call(indexPatternProvider.get, selectedIndex.id); + const selectedIndexPatternId = lookupIndexPatternId(workspacePayload); + const indexPattern = yield call(indexPatternProvider.get, selectedIndexPatternId); const initialSettings = settingsSelector(yield select()); - createWorkspace(selectedIndex.attributes.title, initialSettings); + createWorkspace(indexPattern.title, initialSettings); const { urlTemplates, advancedSettings, allFields } = savedWorkspaceToAppState( - action.payload, + workspacePayload, indexPattern, // workspace won't be null because it's created in the same call stack getWorkspace()! @@ -68,16 +74,16 @@ export const loadingSaga = ({ // put everything in the store yield put( updateMetaData({ - title: action.payload.title, - description: action.payload.description, - savedObjectId: action.payload.id, + title: workspacePayload.title, + description: workspacePayload.description, + savedObjectId: workspacePayload.id, }) ); yield put( setDatasource({ type: 'indexpattern', - id: selectedIndex.id, - title: selectedIndex.attributes.title, + id: indexPattern.id, + title: indexPattern.title, }) ); yield put(loadFields(allFields)); diff --git a/x-pack/plugins/graph/public/types/persistence.ts b/x-pack/plugins/graph/public/types/persistence.ts index 8e7e9c7e8878e..7640ce282a23d 100644 --- a/x-pack/plugins/graph/public/types/persistence.ts +++ b/x-pack/plugins/graph/public/types/persistence.ts @@ -27,10 +27,14 @@ export interface GraphWorkspaceSavedObject { type: string; version?: number; wsState: string; + // the title of the index pattern used by this workspace. + // Only set for legacy saved objects. + legacyIndexPatternRef?: string; _source: Record; } export interface SerializedWorkspaceState { + // the id of the index pattern saved object indexPattern: string; selectedFields: SerializedField[]; blocklist: SerializedNode[]; diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index 85f25542b2efb..7a827db7aba4b 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -60,7 +60,7 @@ export class GraphPlugin implements Plugin { all: ['graph-workspace'], read: ['index-pattern'], }, - ui: ['save', 'delete'], + ui: ['save', 'delete', 'show'], }, read: { app: ['graph', 'kibana'], @@ -69,7 +69,7 @@ export class GraphPlugin implements Plugin { all: [], read: ['index-pattern', 'graph-workspace'], }, - ui: [], + ui: ['show'], }, }, }); diff --git a/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts b/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts index 8e8cb64aac1b9..173ec8f8f6790 100644 --- a/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts +++ b/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts @@ -10,6 +10,20 @@ export const graphWorkspace: SavedObjectsType = { name: 'graph-workspace', namespaceType: 'single', hidden: false, + management: { + icon: 'graphApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/graph#/workspace/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'graph.show', + }; + }, + }, migrations: graphMigrations, mappings: { properties: { @@ -38,6 +52,10 @@ export const graphWorkspace: SavedObjectsType = { wsState: { type: 'text', }, + legacyIndexPatternRef: { + type: 'text', + index: false, + }, }, }, }; diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.test.ts b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts index ecf1f3ca3b69e..bd692b5dc0042 100644 --- a/x-pack/plugins/graph/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts @@ -108,4 +108,96 @@ describe('graph-workspace', () => { `); }); }); + + describe('7.11', () => { + const migration = graphMigrations['7.11.0']; + + test('remove broken reference and set legacy attribute', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify( + JSON.stringify({ foo: true, indexPatternRefName: 'indexPattern_0' }) + ), + bar: true, + }, + references: [ + { + id: 'pattern*', + name: 'indexPattern_0', + type: 'index-pattern', + }, + ], + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "bar": true, + "legacyIndexPatternRef": "pattern*", + "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"", + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('bails out on missing reference', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify( + JSON.stringify({ foo: true, indexPatternRefName: 'indexPattern_0' }) + ), + bar: true, + }, + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toBe(doc); + }); + + test('bails out on missing index pattern in state', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify(JSON.stringify({ foo: true })), + bar: true, + }, + references: [ + { + id: 'pattern*', + name: 'indexPattern_0', + type: 'index-pattern', + }, + ], + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toBe(doc); + }); + + test('bails out on broken wsState', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: '{{[[', + bar: true, + }, + references: [ + { + id: 'pattern*', + name: 'indexPattern_0', + type: 'index-pattern', + }, + ], + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toBe(doc); + }); + }); }); diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts index 34cd59e2220e9..e967a9aeaba70 100644 --- a/x-pack/plugins/graph/server/saved_objects/migrations.ts +++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts @@ -56,4 +56,35 @@ export const graphMigrations = { doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); return doc; }, + '7.11.0': (doc: SavedObjectUnsanitizedDoc) => { + const wsState = get(doc, 'attributes.wsState'); + if (typeof wsState !== 'string') { + return doc; + } + let state; + try { + state = JSON.parse(JSON.parse(wsState)); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + const indexPatternRefName = state.indexPatternRefName; + const indexPatternReference = doc.references?.find( + (reference) => reference.name === indexPatternRefName + ); + if (!indexPatternReference) { + // This saved object doesn't have an reference, there's something corrupted here, + // leave it as is + return doc; + } + const indexPatternTitle = indexPatternReference.id; + // remove index pattern title from workspace state (this should always be the id) + delete state.indexPatternRefName; + // add index pattern title as legacyIndexPatternRef so it can get resolved to the id on next open + doc.attributes.legacyIndexPatternRef = indexPatternTitle; + doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); + // remove references + doc.references = []; + return doc; + }, }; 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 c10b8b7005c7e..8ce7e835dd858 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 @@ -187,12 +187,12 @@ export const setup = async (arg?: { appServicesContext: Partial { - await createFormToggleAction('shrinkSwitch')(true); - await createFormSetValueAction('warm-selectedPrimaryShardCount')(value); + const setShrink = (phase: Phases) => async (value: string) => { + await createFormToggleAction(`${phase}-shrinkSwitch`)(true); + await createFormSetValueAction(`${phase}-selectedPrimaryShardCount`)(value); }; - const shrinkExists = () => exists('shrinkSwitch'); + const shrinkExists = (phase: Phases) => () => exists(`${phase}-shrinkSwitch`); const setFreeze = createFormToggleAction('freezeSwitch'); const freezeExists = () => exists('freezeSwitch'); @@ -237,6 +237,8 @@ export const setup = async (arg?: { appServicesContext: Partial', () => { await actions.hot.toggleForceMerge(true); await actions.hot.setForcemergeSegmentsCount('123'); await actions.hot.setBestCompression(true); + await actions.hot.setShrink('2'); await actions.hot.setIndexPriority('123'); await actions.savePolicy(); @@ -148,6 +149,9 @@ describe('', () => { "set_priority": Object { "priority": 123, }, + "shrink": Object { + "number_of_shards": 2, + }, }, "min_age": "0ms", }, 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 32964ab2ce84d..346176b9c29c3 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 @@ -487,7 +487,7 @@ describe('edit policy', () => { await setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); act(() => { - findTestSubject(rendered, 'shrinkSwitch').simulate('click'); + findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); }); rendered.update(); await setPhaseAfter(rendered, 'warm', '1'); @@ -505,7 +505,7 @@ describe('edit policy', () => { await activatePhase(rendered, 'warm'); await setPhaseAfter(rendered, 'warm', '1'); act(() => { - findTestSubject(rendered, 'shrinkSwitch').simulate('click'); + findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); }); rendered.update(); const shrinkInput = findTestSubject(rendered, 'warm-selectedPrimaryShardCount'); 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 94cc11d0b61a6..1c28262a54305 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -65,6 +65,7 @@ export interface SerializedHotPhase extends SerializedPhase { max_docs?: number; }; forcemerge?: ForcemergeAction; + shrink?: ShrinkAction; set_priority?: { priority: number | null; }; @@ -78,9 +79,7 @@ export interface SerializedHotPhase extends SerializedPhase { export interface SerializedWarmPhase extends SerializedPhase { actions: { allocate?: AllocateAction; - shrink?: { - number_of_shards: number; - }; + shrink?: ShrinkAction; forcemerge?: ForcemergeAction; set_priority?: { priority: number | null; @@ -124,6 +123,10 @@ export interface AllocateAction { }; } +export interface ShrinkAction { + number_of_shards: number; +} + export interface ForcemergeAction { max_num_segments: number; // only accepted value for index_codec 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 5ce4fae596e8e..a4fb03bbd1ca6 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 @@ -37,6 +37,7 @@ import { SetPriorityInputField, SearchableSnapshotField, useRolloverPath, + ShrinkField, } from '../shared_fields'; import { maxSizeStoredUnits, maxAgeUnits } from './constants'; @@ -235,6 +236,7 @@ export const HotPhase: FunctionComponent = () => { {isRolloverEnabled && ( <> {} + {license.canUseSearchableSnapshot() && } )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts index 452abd4c2aeac..503cd65da655b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts @@ -16,4 +16,6 @@ export { MinAgeInputField } from './min_age_input_field'; export { SnapshotPoliciesField } from './snapshot_policies_field'; +export { ShrinkField } from './shrink_field'; + export { SearchableSnapshotField } from './searchable_snapshot_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx new file mode 100644 index 0000000000000..f1cfbeb3692f7 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTextColor } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; + +import { UseField, NumericField } from '../../../../../../shared_imports'; + +import { useEditPolicyContext } from '../../../edit_policy_context'; +import { i18nTexts } from '../../../i18n_texts'; + +import { LearnMoreLink, DescribedFormRow } from '../../'; + +interface Props { + phase: 'hot' | 'warm'; +} + +export const ShrinkField: FunctionComponent = ({ phase }) => { + const path = `phases.${phase}.actions.shrink.number_of_shards`; + const { policy } = useEditPolicyContext(); + return ( + + +

+ } + description={ + + {' '} + + + } + titleSize="xs" + switchProps={{ + 'aria-controls': 'shrinkContent', + 'data-test-subj': `${phase}-shrinkSwitch`, + label: i18nTexts.editPolicy.shrinkLabel, + 'aria-label': i18nTexts.editPolicy.shrinkLabel, + initialValue: Boolean(policy.phases[phase]?.actions?.shrink), + }} + fullWidth + > +
+ + + + + + +
+ + ); +}; 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 index b573bc6a80632..d572e7a2ed341 100644 --- 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 @@ -9,14 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { - EuiTextColor, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiDescribedFormGroup, - EuiAccordion, -} from '@elastic/eui'; +import { EuiSpacer, EuiDescribedFormGroup, EuiAccordion } from '@elastic/eui'; import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports'; @@ -25,7 +18,7 @@ import { Phases } from '../../../../../../../common/types'; import { useEditPolicyContext } from '../../../edit_policy_context'; import { useConfigurationIssues } from '../../../form'; -import { LearnMoreLink, ActiveBadge, DescribedFormRow } from '../../'; +import { ActiveBadge, DescribedFormRow } from '../../'; import { useRolloverPath, @@ -33,12 +26,10 @@ import { ForcemergeField, SetPriorityInputField, DataTierAllocationField, + ShrinkField, } from '../shared_fields'; 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.', @@ -178,55 +169,8 @@ export const WarmPhase: FunctionComponent = () => { }} /> - {!isUsingSearchableSnapshotInHotPhase && ( - - - - } - description={ - - {' '} - - - } - titleSize="xs" - switchProps={{ - 'aria-controls': 'shrinkContent', - 'data-test-subj': 'shrinkSwitch', - label: i18nTexts.shrinkLabel, - 'aria-label': i18nTexts.shrinkLabel, - initialValue: policy.phases.warm?.actions?.shrink != null, - }} - fullWidth - > -
- - - - - - - -
-
- )} + + {!isUsingSearchableSnapshotInHotPhase && } {!isUsingSearchableSnapshotInHotPhase && } {/* Data tier allocation section */} 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 97e4c3ddf4a87..fc7bb16877157 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 @@ -48,6 +48,7 @@ import { import { schema, deserializer, createSerializer, createPolicyNameValidations, Form } from './form'; import { useEditPolicyContext } from './edit_policy_context'; + import { FormInternal } from './types'; export interface Props { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index 20f8423ec24fc..9891921b271e6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -290,4 +290,14 @@ describe('deserializer and serializer', () => { enabled: false, }); }); + + it('removes shrink from hot and warm when unset', () => { + delete formInternal.phases.hot!.actions!.shrink; + delete formInternal.phases.warm!.actions!.shrink; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.shrink).toBeUndefined(); + expect(result.phases.warm!.actions.shrink).toBeUndefined(); + }); }); 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 6485122771a46..73a868c392f32 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 @@ -181,6 +181,25 @@ export const schema: FormSchema = { serializer: serializers.stringToNumber, }, }, + shrink: { + number_of_shards: { + label: i18n.translate('xpack.indexLifecycleMgmt.shrink.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, + }, + }, set_priority: { priority: { defaultValue: defaultSetPriority as any, @@ -216,7 +235,7 @@ export const schema: FormSchema = { }, shrink: { number_of_shards: { - label: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel', { + label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', { defaultMessage: 'Number of primary shards', }), validations: [ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 211c7d263e47e..aaf9963037307 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -69,6 +69,10 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete hotPhaseActions.set_priority; } + if (!updatedPolicy.phases.hot?.actions?.shrink) { + delete hotPhaseActions.shrink; + } + if (!updatedPolicy.phases.hot!.actions?.searchable_snapshot) { delete hotPhaseActions.searchable_snapshot; } 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 f787f2661aa5c..75bd3c3e217af 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 @@ -8,6 +8,9 @@ import { i18n } from '@kbn/i18n'; export const i18nTexts = { editPolicy: { + shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.shrink.indexFieldLabel', { + defaultMessage: 'Shrink index', + }), searchableSnapshotInHotPhase: { searchableSnapshotDisallowed: { calloutTitle: i18n.translate( diff --git a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx b/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx index eae39c9d1b253..9da892ec92ec1 100644 --- a/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx +++ b/x-pack/plugins/infra/public/components/navigation/app_navigation.tsx @@ -24,8 +24,7 @@ export const AppNavigation = ({ 'aria-label': label, children }: AppNavigationPr const Nav = euiStyled.nav` background: ${(props) => props.theme.eui.euiColorEmptyShade}; border-bottom: ${(props) => props.theme.eui.euiBorderThin}; - padding: ${(props) => - `${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL} ${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL}`}; + padding: ${(props) => `${props.theme.eui.euiSizeS} ${props.theme.eui.euiSizeL}`}; .euiTabs { padding-left: 3px; margin-left: -3px; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index 6ee3c9f1fae80..83ba3726dacb9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -4,10 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { + EuiSearchBar, + EuiSpacer, + EuiEmptyPrompt, + EuiButton, + EuiText, + EuiIconTip, + Query, +} from '@elastic/eui'; import { useProcessList, SortBy, @@ -20,6 +28,7 @@ import { ProcessesTable } from './processes_table'; import { parseSearchString } from './parse_search_string'; const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { + const [searchBarState, setSearchBarState] = useState(Query.MATCH_ALL); const [searchFilter, setSearchFilter] = useState(''); const [sortBy, setSortBy] = useState({ name: 'cpu', @@ -45,14 +54,23 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { ); const debouncedSearchOnChange = useMemo( - () => - debounce<(props: { queryText: string }) => void>( - ({ queryText }) => setSearchFilter(queryText), - 500 - ), + () => debounce<(queryText: string) => void>((queryText) => setSearchFilter(queryText), 500), [setSearchFilter] ); + const searchBarOnChange = useCallback( + ({ query, queryText }) => { + setSearchBarState(query); + debouncedSearchOnChange(queryText); + }, + [setSearchBarState, debouncedSearchOnChange] + ); + + const clearSearchBar = useCallback(() => { + setSearchBarState(Query.MATCH_ALL); + setSearchFilter(''); + }, [setSearchBarState, setSearchFilter]); + return ( @@ -61,8 +79,34 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { processSummary={(!error ? response?.summary : null) ?? { total: 0 }} /> + +

+ {i18n.translate('xpack.infra.metrics.nodeDetails.processesHeader', { + defaultMessage: 'Top processes', + })}{' '} + +

+
+ { processList={response?.processList ?? []} sortBy={sortBy} setSortBy={setSortBy} + clearSearchBar={clearSearchBar} /> ) : ( {i18n.translate('xpack.infra.metrics.nodeDetails.processListError', { - defaultMessage: 'Unable to show process data', + defaultMessage: 'Unable to load process data', })} } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx index 1952ba947761c..3e4b066afa157 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -7,6 +7,7 @@ import React, { useMemo, useState, useCallback } from 'react'; import { omit } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTable, EuiTableHeader, @@ -15,6 +16,9 @@ import { EuiTableRowCell, EuiLoadingChart, EuiEmptyPrompt, + EuiText, + EuiLink, + EuiButton, SortableProperties, LEFT_ALIGNMENT, RIGHT_ALIGNMENT, @@ -34,6 +38,7 @@ interface TableProps { isLoading: boolean; sortBy: SortBy; setSortBy: (s: SortBy) => void; + clearSearchBar: () => void; } function useSortableProperties( @@ -66,6 +71,7 @@ export const ProcessesTable = ({ isLoading, sortBy, setSortBy, + clearSearchBar, }: TableProps) => { const { updateSortableProperties } = useSortableProperties( [ @@ -102,13 +108,42 @@ export const ProcessesTable = ({ if (currentItems.length === 0) return ( + {i18n.translate('xpack.infra.metrics.nodeDetails.noProcesses', { - defaultMessage: 'No processes matched these search terms', + defaultMessage: 'No processes found', })} - + + } + body={ + + + +
+ ), + }} + /> + + } + actions={ + + {i18n.translate('xpack.infra.metrics.nodeDetails.noProcessesClearFilters', { + defaultMessage: 'Clear filters', + })} + } /> ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index a1a072be77f81..0f512e535c9d1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -114,8 +114,12 @@ right: 0; } -.lnsLayerPanel__paletteColor { - height: $euiSizeXS; +.lnsLayerPanel__palette { + border-radius: 0 0 ($euiBorderRadius - 1px) ($euiBorderRadius - 1px); + + &::after { + border: none; + } } .lnsLayerPanel__dimensionLink { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx index 7e65fe7025932..b27451236e3b4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx @@ -5,23 +5,18 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiColorPaletteDisplay } from '@elastic/eui'; import { AccessorConfig } from '../../../types'; export function PaletteIndicator({ accessorConfig }: { accessorConfig: AccessorConfig }) { if (accessorConfig.triggerIcon !== 'colorBy' || !accessorConfig.palette) return null; return ( - - {accessorConfig.palette.map((color) => ( - - ))} - +
+ +
); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx index 2fb2bef7f9787..8bceac180f0eb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx @@ -5,7 +5,7 @@ */ import React, { MouseEventHandler } from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { EuiPopover, EuiLink } from '@elastic/eui'; import { createMockedIndexPattern } from '../../../mocks'; @@ -28,8 +28,7 @@ const defaultProps = { Button: ({ onClick }: { onClick: MouseEventHandler }) => ( trigger ), - isOpenByCreation: true, - setIsOpenByCreation: jest.fn(), + initiallyOpen: true, }; describe('filter popover', () => { @@ -39,16 +38,14 @@ describe('filter popover', () => { }, })); it('should be open if is open by creation', () => { - const setIsOpenByCreation = jest.fn(); - const instance = shallow( - - ); + const instance = mount(); + instance.update(); expect(instance.find(EuiPopover).prop('isOpen')).toEqual(true); act(() => { instance.find(EuiPopover).prop('closePopover')!(); }); instance.update(); - expect(setIsOpenByCreation).toHaveBeenCalledWith(false); + expect(instance.find(EuiPopover).prop('isOpen')).toEqual(false); }); it('should call setFilter when modifying QueryInput', () => { const setFilter = jest.fn(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx index ca84c072be5ce..df01b8e4b4afc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -5,7 +5,7 @@ */ import './filter_popover.scss'; -import React, { MouseEventHandler, useState } from 'react'; +import React, { MouseEventHandler, useEffect, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { EuiPopover, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -19,23 +19,24 @@ export const FilterPopover = ({ setFilter, indexPattern, Button, - isOpenByCreation, - setIsOpenByCreation, + initiallyOpen, }: { filter: FilterValue; setFilter: Function; indexPattern: IndexPattern; Button: React.FunctionComponent<{ onClick: MouseEventHandler }>; - isOpenByCreation: boolean; - setIsOpenByCreation: Function; + initiallyOpen: boolean; }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const inputRef = React.useRef(); + // set popover open on start to work around EUI bug + useEffect(() => { + setIsPopoverOpen(initiallyOpen); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const closePopover = () => { - if (isOpenByCreation) { - setIsOpenByCreation(false); - } if (isPopoverOpen) { setIsPopoverOpen(false); } @@ -59,15 +60,12 @@ export const FilterPopover = ({ data-test-subj="indexPattern-filters-existingFilterContainer" anchorClassName="eui-fullWidth" panelClassName="lnsIndexPatternDimensionEditor__filtersEditor" - isOpen={isOpenByCreation || isPopoverOpen} + isOpen={isPopoverOpen} ownFocus closePopover={() => closePopover()} button={ -
-
- - + + + + + + +
+ +
+
+ + + +
+
+ +
+
+ -
-
+ -
- - - - - - + + ); } ); + +const SchemaInformation = ({ + closePopover, + setActivePopover, + isOpen, +}: { + closePopover: () => void; + setActivePopover: (value: 'schemaInfo' | null) => void; + isOpen: boolean; +}) => { + const colorMap = useColors(); + const sourceAndSchema = useSelector(selectors.resolverTreeSourceAndSchema); + const setAsActivePopover = useCallback(() => setActivePopover('schemaInfo'), [setActivePopover]); + + const schemaInfoButtonTitle = i18n.translate( + 'xpack.securitySolution.resolver.graphControls.schemaInfoButtonTitle', + { + defaultMessage: 'Schema Information', + } + ); + + const unknownSchemaValue = i18n.translate( + 'xpack.securitySolution.resolver.graphControls.unknownSchemaValue', + { + defaultMessage: 'Unknown', + } + ); + + return ( + + } + isOpen={isOpen} + closePopover={closePopover} + anchorPosition="leftCenter" + > + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaInfoTitle', { + defaultMessage: 'process tree', + })} + + +
+ + <> + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaSource', { + defaultMessage: 'source', + })} + + + {sourceAndSchema?.dataSource ?? unknownSchemaValue} + + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaID', { + defaultMessage: 'id', + })} + + + {sourceAndSchema?.schema.id ?? unknownSchemaValue} + + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaEdge', { + defaultMessage: 'edge', + })} + + + {sourceAndSchema?.schema.parent ?? unknownSchemaValue} + + + +
+
+ ); +}; + +// This component defines the cube legend that allows users to identify the meaning of the cubes +// Should be updated to be dynamic if and when non process based resolvers are possible +const NodeLegend = ({ + closePopover, + setActivePopover, + isOpen, +}: { + closePopover: () => void; + setActivePopover: (value: 'nodeLegend') => void; + isOpen: boolean; +}) => { + const setAsActivePopover = useCallback(() => setActivePopover('nodeLegend'), [setActivePopover]); + const colorMap = useColors(); + + const nodeLegendButtonTitle = i18n.translate( + 'xpack.securitySolution.resolver.graphControls.nodeLegendButtonTitle', + { + defaultMessage: 'Node Legend', + } + ); + + return ( + + } + isOpen={isOpen} + closePopover={closePopover} + anchorPosition="leftCenter" + > + + {i18n.translate('xpack.securitySolution.resolver.graphControls.nodeLegend', { + defaultMessage: 'legend', + })} + +
+ + <> + + + + + + {i18n.translate( + 'xpack.securitySolution.resolver.graphControls.runningProcessCube', + { + defaultMessage: 'Running Process', + } + )} + + + + + + + + {i18n.translate( + 'xpack.securitySolution.resolver.graphControls.terminatedProcessCube', + { + defaultMessage: 'Terminated Process', + } + )} + + + + + + + + {i18n.translate( + 'xpack.securitySolution.resolver.graphControls.currentlyLoadingCube', + { + defaultMessage: 'Loading Process', + } + )} + + + + + + + + {i18n.translate('xpack.securitySolution.resolver.graphControls.errorCube', { + defaultMessage: 'Error', + })} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx index cc5f39e985d9e..99c57757fbb6a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx @@ -17,40 +17,44 @@ interface StyledSVGCube { } import { useCubeAssets } from '../use_cube_assets'; import { useSymbolIDs } from '../use_symbol_ids'; +import { NodeDataStatus } from '../../types'; /** * Icon representing a process node. */ export const CubeForProcess = memo(function ({ className, - running, + size = '2.15em', + state, isOrigin, 'data-test-subj': dataTestSubj, }: { 'data-test-subj'?: string; /** - * True if the process represented by the node is still running. + * The state of the process's node data (for endpoint the process's lifecycle events) */ - running: boolean; + state: NodeDataStatus; + /** The css size (px, em, etc...) for the width and height of the svg cube. Defaults to 2.15em */ + size?: string; isOrigin?: boolean; className?: string; }) { - const { cubeSymbol, strokeColor } = useCubeAssets(!running, false); + const { cubeSymbol, strokeColor } = useCubeAssets(state, false); const { processCubeActiveBacking } = useSymbolIDs(); return ( {i18n.translate('xpack.securitySolution.resolver.node_icon', { - defaultMessage: '{running, select, true {Running Process} false {Terminated Process}}', - values: { running }, + defaultMessage: `{state, select, running {Running Process} terminated {Terminated Process} loading {Loading Process} error {Error Process}}`, + values: { state }, })} {isOrigin && ( 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 4936cf0cbb80e..003182bd5f1b7 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 @@ -29,6 +29,7 @@ import { useLinkProps } from '../use_link_props'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { deepObjectEntries } from './deep_object_entries'; import { useFormattedDate } from './use_formatted_date'; +import * as nodeDataModel from '../../models/node_data'; const eventDetailRequestError = i18n.translate( 'xpack.securitySolution.resolver.panel.eventDetail.requestError', @@ -39,23 +40,24 @@ const eventDetailRequestError = i18n.translate( export const EventDetail = memo(function EventDetail({ nodeID, - eventID, eventCategory: eventType, }: { nodeID: string; - eventID: string; /** The event type to show in the breadcrumbs */ eventCategory: string; }) { const isEventLoading = useSelector(selectors.isCurrentRelatedEventLoading); - const isProcessTreeLoading = useSelector(selectors.isTreeLoading); + const isTreeLoading = useSelector(selectors.isTreeLoading); + const processEvent = useSelector((state: ResolverState) => + nodeDataModel.firstEvent(selectors.nodeDataForID(state)(nodeID)) + ); + const nodeStatus = useSelector((state: ResolverState) => selectors.nodeDataStatus(state)(nodeID)); - const isLoading = isEventLoading || isProcessTreeLoading; + const isNodeDataLoading = nodeStatus === 'loading'; + const isLoading = isEventLoading || isTreeLoading || isNodeDataLoading; const event = useSelector(selectors.currentRelatedEventData); - const processEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) - ); + return isLoading ? ( @@ -90,7 +92,7 @@ const EventDetailContents = memo(function ({ * Event type to use in the breadcrumbs */ eventType: string; - processEvent: SafeResolverEvent | null; + processEvent: SafeResolverEvent | undefined; }) { const timestamp = eventModel.timestampSafeVersion(event); const formattedDate = diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx index f6fbd280e7ed5..c6e81f691e2fe 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx @@ -37,7 +37,6 @@ export const PanelRouter = memo(function () { return ( ); 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 27a7723d7d656..fedf1ae2499ae 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 @@ -20,6 +20,7 @@ import { GeneratedText } from '../generated_text'; import { CopyablePanelField } from './copyable_panel_field'; import { Breadcrumbs } from './breadcrumbs'; import { processPath, processPID } from '../../models/process_event'; +import * as nodeDataModel from '../../models/node_data'; import { CubeForProcess } from './cube_for_process'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { useCubeAssets } from '../use_cube_assets'; @@ -28,28 +29,35 @@ import { PanelLoading } from './panel_loading'; import { StyledPanel } from '../styles'; import { useLinkProps } from '../use_link_props'; import { useFormattedDate } from './use_formatted_date'; +import { PanelContentError } from './panel_content_error'; const StyledCubeForProcess = styled(CubeForProcess)` position: relative; top: 0.75em; `; +const nodeDetailError = i18n.translate('xpack.securitySolution.resolver.panel.nodeDetail.Error', { + defaultMessage: 'Node details were unable to be retrieved', +}); + export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) { const processEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) + nodeDataModel.firstEvent(selectors.nodeDataForID(state)(nodeID)) ); - return ( - <> - {processEvent === null ? ( - - - - ) : ( - - - - )} - + const nodeStatus = useSelector((state: ResolverState) => selectors.nodeDataStatus(state)(nodeID)); + + return nodeStatus === 'loading' ? ( + + + + ) : processEvent ? ( + + + + ) : ( + + + ); }); @@ -65,9 +73,7 @@ const NodeDetailView = memo(function ({ nodeID: string; }) { const processName = eventModel.processNameSafeVersion(processEvent); - const isProcessTerminated = useSelector((state: ResolverState) => - selectors.isProcessTerminated(state)(nodeID) - ); + const nodeState = useSelector((state: ResolverState) => selectors.nodeDataStatus(state)(nodeID)); const relatedEventTotal = useSelector((state: ResolverState) => { return selectors.relatedEventTotalCount(state)(nodeID); }); @@ -171,7 +177,7 @@ const NodeDetailView = memo(function ({ }, ]; }, [processName, nodesLinkNavProps]); - const { descriptionText } = useCubeAssets(isProcessTerminated, false); + const { descriptionText } = useCubeAssets(nodeState, false); const nodeDetailNavProps = useLinkProps({ panelView: 'nodeEvents', @@ -187,7 +193,7 @@ const NodeDetailView = memo(function ({ {processName} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx index d0601fad43f57..6f0c336ab3df4 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx @@ -13,21 +13,21 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { useSelector } from 'react-redux'; import { Breadcrumbs } from './breadcrumbs'; import * as event from '../../../../common/endpoint/models/event'; -import { ResolverNodeStats } from '../../../../common/endpoint/types'; +import { EventStats } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; import { ResolverState } from '../../types'; import { StyledPanel } from '../styles'; import { PanelLoading } from './panel_loading'; import { useLinkProps } from '../use_link_props'; +import * as nodeDataModel from '../../models/node_data'; export function NodeEvents({ nodeID }: { nodeID: string }) { const processEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) + nodeDataModel.firstEvent(selectors.nodeDataForID(state)(nodeID)) ); - const relatedEventsStats = useSelector((state: ResolverState) => - selectors.relatedEventsStats(state)(nodeID) - ); - if (processEvent === null || relatedEventsStats === undefined) { + const nodeStats = useSelector((state: ResolverState) => selectors.nodeStats(state)(nodeID)); + + if (processEvent === undefined || nodeStats === undefined) { return ( @@ -39,10 +39,10 @@ export function NodeEvents({ nodeID }: { nodeID: string }) { - + ); } @@ -64,7 +64,7 @@ const EventCategoryLinks = memo(function ({ relatedStats, }: { nodeID: string; - relatedStats: ResolverNodeStats; + relatedStats: EventStats; }) { interface EventCountsTableView { eventType: string; @@ -72,7 +72,7 @@ const EventCategoryLinks = memo(function ({ } const rows = useMemo(() => { - return Object.entries(relatedStats.events.byCategory).map( + return Object.entries(relatedStats.byCategory).map( ([eventType, count]): EventCountsTableView => { return { eventType, @@ -80,7 +80,7 @@ const EventCategoryLinks = memo(function ({ }; } ); - }, [relatedStats.events.byCategory]); + }, [relatedStats.byCategory]); const columns = useMemo>>( () => [ 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 c9648c6f562e5..fbfba38295ea4 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 @@ -42,9 +42,7 @@ export const NodeEventsInCategory = memo(function ({ nodeID: string; eventCategory: string; }) { - const processEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) - ); + const node = useSelector((state: ResolverState) => selectors.graphNodeForID(state)(nodeID)); const eventCount = useSelector((state: ResolverState) => selectors.totalRelatedEventCountForNode(state)(nodeID) ); @@ -57,13 +55,13 @@ export const NodeEventsInCategory = memo(function ({ const hasError = useSelector(selectors.hadErrorLoadingNodeEventsInCategory); return ( <> - {isLoading || processEvent === null ? ( + {isLoading ? ( ) : ( - {hasError ? ( + {hasError || !node ? ( { useCallback((state: ResolverState) => { const { processNodePositions } = selectors.layout(state); const view: ProcessTableView[] = []; - for (const processEvent of processNodePositions.keys()) { - const name = eventModel.processNameSafeVersion(processEvent); - const nodeID = eventModel.entityIDSafeVersion(processEvent); + for (const treeNode of processNodePositions.keys()) { + const name = nodeModel.nodeName(treeNode); + const nodeID = nodeModel.nodeID(treeNode); if (nodeID !== undefined) { view.push({ name, - timestamp: eventModel.timestampAsDateSafeVersion(processEvent), + timestamp: nodeModel.timestampAsDate(treeNode), nodeID, }); } @@ -119,7 +119,8 @@ export const NodeList = memo(() => { const children = useSelector(selectors.hasMoreChildren); const ancestors = useSelector(selectors.hasMoreAncestors); - const showWarning = children === true || ancestors === true; + const generations = useSelector(selectors.hasMoreGenerations); + const showWarning = children === true || ancestors === true || generations === true; const rowProps = useMemo(() => ({ 'data-test-subj': 'resolver:node-list:item' }), []); return ( @@ -141,9 +142,7 @@ function NodeDetailLink({ name, nodeID }: { name?: string; nodeID: string }) { const isOrigin = useSelector((state: ResolverState) => { return selectors.originID(state) === nodeID; }); - const isTerminated = useSelector((state: ResolverState) => - nodeID === undefined ? false : selectors.isProcessTerminated(state)(nodeID) - ); + const nodeState = useSelector((state: ResolverState) => selectors.nodeDataStatus(state)(nodeID)); const { descriptionText } = useColors(); const linkProps = useLinkProps({ panelView: 'nodeDetail', panelParameters: { nodeID } }); const dispatch: (action: ResolverAction) => void = useDispatch(); @@ -162,7 +161,12 @@ function NodeDetailLink({ name, nodeID }: { name?: string; nodeID: string }) { [timestamp, linkProps, dispatch, nodeID] ); return ( - + {name === undefined ? ( {i18n.translate( @@ -175,7 +179,7 @@ function NodeDetailLink({ name, nodeID }: { name?: string; nodeID: string }) { ) : ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx index 39a5130ecaf68..6f20063d10d0a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_states.test.tsx @@ -23,6 +23,8 @@ describe('Resolver: panel loading and resolution states', () => { nodeID: 'origin', eventCategory: 'registry', eventID: firstRelatedEventID, + eventTimestamp: '0', + winlogRecordID: '0', }, panelView: 'eventDetail', }); @@ -129,7 +131,7 @@ describe('Resolver: panel loading and resolution states', () => { }); describe('when navigating to the event categories panel', () => { - let resumeRequest: (pausableRequest: ['entities']) => void; + let resumeRequest: (pausableRequest: ['eventsWithEntityIDAndCategory']) => void; beforeEach(() => { const { metadata: { databaseDocumentID }, @@ -140,7 +142,7 @@ describe('Resolver: panel loading and resolution states', () => { resumeRequest = resume; memoryHistory = createMemoryHistory(); - pause(['entities']); + pause(['eventsWithEntityIDAndCategory']); simulator = new Simulator({ dataAccessLayer, @@ -170,7 +172,7 @@ describe('Resolver: panel loading and resolution states', () => { }); it('should successfully load the events in category panel', async () => { - await resumeRequest(['entities']); + await resumeRequest(['eventsWithEntityIDAndCategory']); await expect( simulator.map(() => ({ resolverPanelLoading: simulator.testSubject('resolver:panel:loading').length, @@ -186,7 +188,7 @@ describe('Resolver: panel loading and resolution states', () => { }); describe('when navigating to the node detail panel', () => { - let resumeRequest: (pausableRequest: ['entities']) => void; + let resumeRequest: (pausableRequest: ['nodeData']) => void; beforeEach(() => { const { metadata: { databaseDocumentID }, @@ -197,7 +199,7 @@ describe('Resolver: panel loading and resolution states', () => { resumeRequest = resume; memoryHistory = createMemoryHistory(); - pause(['entities']); + pause(['nodeData']); simulator = new Simulator({ dataAccessLayer, @@ -226,7 +228,7 @@ describe('Resolver: panel loading and resolution states', () => { }); it('should successfully load the events in category panel', async () => { - await resumeRequest(['entities']); + await resumeRequest(['nodeData']); await expect( simulator.map(() => ({ resolverPanelLoading: simulator.testSubject('resolver:panel:loading').length, diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 7a3657fe93514..ab6083c796b3a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -9,12 +9,13 @@ import styled from 'styled-components'; import { htmlIdGenerator, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { NodeSubMenu } from './styles'; import { applyMatrix3 } from '../models/vector2'; import { Vector2, Matrix3, ResolverState } from '../types'; -import { SafeResolverEvent } from '../../../common/endpoint/types'; +import { ResolverNode } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; -import * as eventModel from '../../../common/endpoint/models/event'; +import * as nodeModel from '../../../common/endpoint/models/node'; import * as selectors from '../store/selectors'; import { fontSize } from './font_size'; import { useCubeAssets } from './use_cube_assets'; @@ -65,9 +66,50 @@ const StyledDescriptionText = styled.div` z-index: 45; `; -const StyledOuterGroup = styled.g` +interface StyledEuiButtonContent { + readonly isShowingIcon: boolean; +} + +const StyledEuiButtonContent = styled.span` + padding: ${(props) => (props.isShowingIcon ? '0px' : '0 12px')}; +`; + +const StyledOuterGroup = styled.g<{ isNodeLoading: boolean }>` fill: none; pointer-events: visiblePainted; + // The below will apply the loading css to the element that references the cube + // when the nodeData is loading for the current node + ${(props) => + props.isNodeLoading && + ` + & .cube { + animation-name: pulse; + /** + * his is a multiple of .6 so it can match up with the EUI button's loading spinner + * which is (0.6s). Using .6 here makes it a bit too fast. + */ + animation-duration: 1.8s; + animation-delay: 0; + animation-direction: normal; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + + /** + * Animation loading state of the cube. + */ + @keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.35; + } + 100% { + opacity: 1; + } + } + `} `; /** @@ -77,9 +119,9 @@ const UnstyledProcessEventDot = React.memo( ({ className, position, - event, + node, + nodeID, projectionMatrix, - isProcessTerminated, timeAtRender, }: { /** @@ -87,21 +129,21 @@ const UnstyledProcessEventDot = React.memo( */ className?: string; /** - * The positon of the process node, in 'world' coordinates. + * The positon of the graph node, in 'world' coordinates. */ position: Vector2; /** - * An event which contains details about the process node. + * An event which contains details about the graph node. */ - event: SafeResolverEvent; + node: ResolverNode; /** - * projectionMatrix which can be used to convert `position` to screen coordinates. + * The unique identifier for the node based on a datasource id */ - projectionMatrix: Matrix3; + nodeID: string; /** - * Whether or not to show the process as terminated. + * projectionMatrix which can be used to convert `position` to screen coordinates. */ - isProcessTerminated: boolean; + projectionMatrix: Matrix3; /** * The time (unix epoch) at render. @@ -125,14 +167,7 @@ const UnstyledProcessEventDot = React.memo( const ariaActiveDescendant = useSelector(selectors.ariaActiveDescendant); const selectedNode = useSelector(selectors.selectedNode); const originID = useSelector(selectors.originID); - const nodeID: string | undefined = eventModel.entityIDSafeVersion(event); - if (nodeID === undefined) { - // NB: this component should be taking nodeID as a `string` instead of handling this logic here - throw new Error('Tried to render a node with no ID'); - } - const relatedEventStats = useSelector((state: ResolverState) => - selectors.relatedEventsStats(state)(nodeID) - ); + const nodeStats = useSelector((state: ResolverState) => selectors.nodeStats(state)(nodeID)); // define a standard way of giving HTML IDs to nodes based on their entity_id/nodeID. // this is used to link nodes via aria attributes @@ -218,6 +253,11 @@ const UnstyledProcessEventDot = React.memo( | null; } = React.createRef(); const colorMap = useColors(); + + const nodeState = useSelector((state: ResolverState) => + selectors.nodeDataStatus(state)(nodeID) + ); + const isNodeLoading = nodeState === 'loading'; const { backingFill, cubeSymbol, @@ -226,7 +266,7 @@ const UnstyledProcessEventDot = React.memo( labelButtonFill, strokeColor, } = useCubeAssets( - isProcessTerminated, + nodeState, /** * There is no definition for 'trigger process' yet. return false. */ false @@ -257,19 +297,29 @@ const UnstyledProcessEventDot = React.memo( if (animationTarget.current?.beginElement) { animationTarget.current.beginElement(); } - dispatch({ - type: 'userSelectedResolverNode', - payload: nodeID, - }); - processDetailNavProps.onClick(clickEvent); + + if (nodeState === 'error') { + dispatch({ + type: 'userReloadedResolverNode', + payload: nodeID, + }); + } else { + dispatch({ + type: 'userSelectedResolverNode', + payload: nodeID, + }); + processDetailNavProps.onClick(clickEvent); + } }, - [animationTarget, dispatch, nodeID, processDetailNavProps] + [animationTarget, dispatch, nodeID, processDetailNavProps, nodeState] ); const grandTotal: number | null = useSelector((state: ResolverState) => - selectors.relatedEventTotalForProcess(state)(event) + selectors.statsTotalForNode(state)(node) ); + const nodeName = nodeModel.nodeName(node); + /* eslint-disable jsx-a11y/click-events-have-key-events */ /** * Key event handling (e.g. 'Enter'/'Space') is provisioned by the `EuiKeyboardAccessible` component @@ -315,7 +365,7 @@ const UnstyledProcessEventDot = React.memo( zIndex: 30, }} > - + - + - {eventModel.processNameSafeVersion(event)} + {i18n.translate('xpack.securitySolution.resolver.node_button_name', { + defaultMessage: `{nodeState, select, error {Reload {nodeName}} other {{nodeName}}}`, + values: { + nodeState, + nodeName, + }, + })} - + 0 && ( )} diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx index d8d8de640d786..fa1686e7ea4b6 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx @@ -35,12 +35,12 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, })) ).toYieldEqualTo({ resolverGraphLoading: 1, resolverGraphError: 0, - resolverGraph: 0, + resolverTree: 0, }); }); }); @@ -66,12 +66,12 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, })) ).toYieldEqualTo({ resolverGraphLoading: 1, resolverGraphError: 0, - resolverGraph: 0, + resolverTree: 0, }); }); }); @@ -96,12 +96,12 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, })) ).toYieldEqualTo({ resolverGraphLoading: 0, resolverGraphError: 1, - resolverGraph: 0, + resolverTree: 0, }); }); }); @@ -126,13 +126,13 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, resolverGraphNodes: simulator.testSubject('resolver:node').length, })) ).toYieldEqualTo({ resolverGraphLoading: 0, resolverGraphError: 0, - resolverGraph: 1, + resolverTree: 1, resolverGraphNodes: 0, }); }); @@ -158,13 +158,13 @@ describe('Resolver: data loading and resolution states', () => { simulator.map(() => ({ resolverGraphLoading: simulator.testSubject('resolver:graph:loading').length, resolverGraphError: simulator.testSubject('resolver:graph:error').length, - resolverGraph: simulator.testSubject('resolver:graph').length, + resolverTree: simulator.testSubject('resolver:graph').length, resolverGraphNodes: simulator.testSubject('resolver:node').length, })) ).toYieldEqualTo({ resolverGraphLoading: 0, resolverGraphError: 0, - resolverGraph: 1, + resolverTree: 1, resolverGraphNodes: 3, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index ed969b913a72e..65b72cf4bfa77 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -19,7 +19,7 @@ import { useCamera } from './use_camera'; import { SymbolDefinitions } from './symbol_definitions'; import { useStateSyncingActions } from './use_state_syncing_actions'; import { StyledMapContainer, GraphContainer } from './styles'; -import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; +import * as nodeModel from '../../../common/endpoint/models/node'; import { SideEffectContext } from './side_effect_context'; import { ResolverProps, ResolverState } from '../types'; import { PanelRouter } from './panels'; @@ -54,7 +54,7 @@ export const ResolverWithoutProviders = React.memo( } = useSelector((state: ResolverState) => selectors.visibleNodesAndEdgeLines(state)(timeAtRender) ); - const terminatedProcesses = useSelector(selectors.terminatedProcesses); + const { projectionMatrix, ref: cameraRef, onMouseDown } = useCamera(); const ref = useCallback( @@ -113,15 +113,18 @@ export const ResolverWithoutProviders = React.memo( /> ) )} - {[...processNodePositions].map(([processEvent, position]) => { - const processEntityId = entityIDSafeVersion(processEvent); + {[...processNodePositions].map(([treeNode, position]) => { + const nodeID = nodeModel.nodeID(treeNode); + if (nodeID === undefined) { + throw new Error('Tried to render a node without an ID'); + } return ( ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 6312991ddb743..e24c4b5664e42 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import { EuiI18nNumber } from '@elastic/eui'; -import { ResolverNodeStats } from '../../../common/endpoint/types'; +import { EventStats } from '../../../common/endpoint/types'; import { useRelatedEventByCategoryNavigation } from './use_related_event_by_category_navigation'; import { useColors } from './use_colors'; @@ -67,7 +67,7 @@ export const NodeSubMenuComponents = React.memo( ({ className, nodeID, - relatedEventStats, + nodeStats, }: { className?: string; // eslint-disable-next-line react/no-unused-prop-types @@ -76,18 +76,18 @@ export const NodeSubMenuComponents = React.memo( * Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself. */ nodeID: string; - relatedEventStats: ResolverNodeStats | undefined; + nodeStats: EventStats | undefined; }) => { // The last projection matrix that was used to position the popover const relatedEventCallbacks = useRelatedEventByCategoryNavigation({ nodeID, - categories: relatedEventStats?.events?.byCategory, + categories: nodeStats?.byCategory, }); const relatedEventOptions = useMemo(() => { - if (relatedEventStats === undefined) { + if (nodeStats === undefined) { return []; } else { - return Object.entries(relatedEventStats.events.byCategory).map(([category, total]) => { + return Object.entries(nodeStats.byCategory).map(([category, total]) => { const [mantissa, scale, hasRemainder] = compactNotationParts(total || 0); const prefix = ( { diff --git a/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx b/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx index edf551c6cbeb9..b06cce11661e8 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx @@ -8,10 +8,59 @@ import React, { memo } from 'react'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; import { useSymbolIDs } from './use_symbol_ids'; import { usePaintServerIDs } from './use_paint_server_ids'; +const loadingProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.loadingProcess', + { + defaultMessage: 'Loading Process', + } +); + +const errorProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.errorProcess', + { + defaultMessage: 'Error Process', + } +); + +const runningProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.runningProcess', + { + defaultMessage: 'Running Process', + } +); + +const triggerProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.triggerProcess', + { + defaultMessage: 'Trigger Process', + } +); + +const terminatedProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.terminatedProcess', + { + defaultMessage: 'Terminated Process', + } +); + +const terminatedTriggerProcessTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.terminatedTriggerProcess', + { + defaultMessage: 'Terminated Trigger Process', + } +); + +const hoveredProcessBackgroundTitle = i18n.translate( + 'xpack.securitySolution.resolver.symbolDefinitions.hoveredProcessBackground', + { + defaultMessage: 'Hovered Process Background', + } +); /** * PaintServers: Where color palettes, gradients, patterns and other similar concerns * are exposed to the component @@ -20,6 +69,17 @@ const PaintServers = memo(({ isDarkMode }: { isDarkMode: boolean }) => { const paintServerIDs = usePaintServerIDs(); return ( <> + + + + { paintOrder="normal" /> + + {loadingProcessTitle} + + + + {errorProcessTitle} + + + + + + + - {'Running Process'} + {runningProcessTitle} { /> - {'resolver_dark process running'} + {triggerProcessTitle} { /> - {'Terminated Process'} + {terminatedProcessTitle} { - {'Terminated Trigger Process'} + {terminatedTriggerProcessTitle} {isDarkMode && ( { - {'resolver active backing'} + {hoveredProcessBackgroundTitle} { /** Enzyme full DOM wrapper for the element the camera is attached to. */ @@ -247,43 +248,48 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: SafeResolverEvent; + let node: ResolverNode; beforeEach(async () => { - const events: SafeResolverEvent[] = []; - const numberOfEvents: number = 10; + const nodes: ResolverNode[] = []; + const numberOfNodes: number = 10; - for (let index = 0; index < numberOfEvents; index++) { - const uniquePpid = index === 0 ? undefined : index - 1; - events.push( - mockProcessEvent({ - endgame: { - unique_pid: index, - unique_ppid: uniquePpid, - event_type_full: 'process_event', - event_subtype_full: 'creation_event', - }, + for (let index = 0; index < numberOfNodes; index++) { + const parentID = index === 0 ? undefined : String(index - 1); + nodes.push( + mockResolverNode({ + id: String(index), + name: '', + parentID, + timestamp: 0, + stats: { total: 0, byCategory: {} }, }) ); } - const tree = mockResolverTree({ events }); + const tree = mockResolverTree({ nodes }); if (tree !== null) { + const { schema, dataSource } = endpointSourceSchema(); const serverResponseAction: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: tree, parameters: mockTreeFetcherParameters() }, + payload: { + result: tree, + dataSource, + schema, + parameters: mockTreeFetcherParameters(), + }, }; store.dispatch(serverResponseAction); } else { throw new Error('failed to create tree'); } - const processes: SafeResolverEvent[] = [ + const resolverNodes: ResolverNode[] = [ ...selectors.layout(store.getState()).processNodePositions.keys(), ]; - process = processes[processes.length - 1]; + node = resolverNodes[resolverNodes.length - 1]; if (!process) { throw new Error('missing the process to bring into view'); } simulator.controls.time = 0; - const nodeID = entityIDSafeVersion(process); + const nodeID = nodeModel.nodeID(node); if (!nodeID) { throw new Error('could not find nodeID for process'); } diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts index 7daf181a7b2bb..90ce5dc22d177 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts @@ -15,6 +15,7 @@ type ResolverColorNames = | 'full' | 'graphControls' | 'graphControlsBackground' + | 'graphControlsBorderColor' | 'linkColor' | 'resolverBackground' | 'resolverEdge' @@ -38,6 +39,7 @@ export function useColors(): ColorMap { full: theme.euiColorFullShade, graphControls: theme.euiColorDarkestShade, graphControlsBackground: theme.euiColorEmptyShade, + graphControlsBorderColor: theme.euiColorLightShade, processBackingFill: `${theme.euiColorPrimary}${isDarkMode ? '1F' : '0F'}`, // Add opacity 0F = 6% , 1F = 12% resolverBackground: theme.euiColorEmptyShade, resolverEdge: isDarkMode ? theme.euiColorLightShade : theme.euiColorLightestShade, diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts index c743ebc43f2be..94f08c5f3fee3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts @@ -10,7 +10,7 @@ import { ButtonColor } from '@elastic/eui'; import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; import { useMemo } from 'react'; -import { ResolverProcessType } from '../types'; +import { ResolverProcessType, NodeDataStatus } from '../types'; import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; import { useSymbolIDs } from './use_symbol_ids'; import { useColors } from './use_colors'; @@ -19,7 +19,7 @@ import { useColors } from './use_colors'; * Provides colors and HTML IDs used to render the 'cube' graphic that accompanies nodes. */ export function useCubeAssets( - isProcessTerminated: boolean, + cubeType: NodeDataStatus, isProcessTrigger: boolean ): NodeStyleConfig { const SymbolIds = useSymbolIDs(); @@ -40,6 +40,28 @@ export function useCubeAssets( labelButtonFill: 'primary', strokeColor: theme.euiColorPrimary, }, + loadingCube: { + backingFill: colorMap.processBackingFill, + cubeSymbol: `#${SymbolIds.loadingCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.loadingProcess', { + defaultMessage: 'Loading Process', + }), + isLabelFilled: false, + labelButtonFill: 'primary', + strokeColor: theme.euiColorPrimary, + }, + errorCube: { + backingFill: colorMap.processBackingFill, + cubeSymbol: `#${SymbolIds.errorCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.errorProcess', { + defaultMessage: 'Error Process', + }), + isLabelFilled: false, + labelButtonFill: 'primary', + strokeColor: theme.euiColorPrimary, + }, runningTriggerCube: { backingFill: colorMap.triggerBackingFill, cubeSymbol: `#${SymbolIds.runningTriggerCube}`, @@ -83,16 +105,22 @@ export function useCubeAssets( [SymbolIds, colorMap, theme] ); - if (isProcessTerminated) { + if (cubeType === 'terminated') { if (isProcessTrigger) { return nodeAssets.terminatedTriggerCube; } else { return nodeAssets[processTypeToCube.processTerminated]; } - } else if (isProcessTrigger) { - return nodeAssets[processTypeToCube.processCausedAlert]; + } else if (cubeType === 'running') { + if (isProcessTrigger) { + return nodeAssets[processTypeToCube.processCausedAlert]; + } else { + return nodeAssets[processTypeToCube.processRan]; + } + } else if (cubeType === 'error') { + return nodeAssets[processTypeToCube.processError]; } else { - return nodeAssets[processTypeToCube.processRan]; + return nodeAssets[processTypeToCube.processLoading]; } } @@ -102,6 +130,8 @@ const processTypeToCube: Record = { processTerminated: 'terminatedProcessCube', unknownProcessEvent: 'runningProcessCube', processCausedAlert: 'runningTriggerCube', + processLoading: 'loadingCube', + processError: 'errorCube', unknownEvent: 'runningProcessCube', }; interface NodeStyleMap { @@ -109,6 +139,8 @@ interface NodeStyleMap { runningTriggerCube: NodeStyleConfig; terminatedProcessCube: NodeStyleConfig; terminatedTriggerCube: NodeStyleConfig; + loadingCube: NodeStyleConfig; + errorCube: NodeStyleConfig; } interface NodeStyleConfig { backingFill: string; diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts b/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts index 0336a29bb0721..10fbd58a9deb3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts @@ -23,6 +23,8 @@ export function usePaintServerIDs() { runningTriggerCube: `${prefix}-psRunningTriggerCube`, terminatedProcessCube: `${prefix}-psTerminatedProcessCube`, terminatedTriggerCube: `${prefix}-psTerminatedTriggerCube`, + loadingCube: `${prefix}-psLoadingCube`, + errorCube: `${prefix}-psErrorCube`, }; }, [resolverComponentInstanceID]); } diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts b/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts index 0e1fd5737a3ce..da00d4c0dbf43 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts @@ -25,6 +25,8 @@ export function useSymbolIDs() { terminatedProcessCube: `${prefix}-terminatedCube`, terminatedTriggerCube: `${prefix}-terminatedTriggerCube`, processCubeActiveBacking: `${prefix}-activeBacking`, + loadingCube: `${prefix}-loadingCube`, + errorCube: `${prefix}-errorCube`, }; }, [resolverComponentInstanceID]); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 5b558df8388e4..b53c11868998f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -72,7 +72,7 @@ const NavigationComponent: React.FC = ({ timelineFullScreen, toggleFullScreen, }) => ( - + {i18n.CLOSE_ANALYZER} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 5a1540b970300..6c76da44c8557 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -312,10 +312,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -411,10 +413,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.template, @@ -510,10 +514,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -607,10 +613,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, title: '', timelineType: TimelineType.default, @@ -745,10 +753,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, id: 'savedObject-1', }); @@ -912,10 +922,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.draft, id: 'savedObject-1', }); @@ -1007,10 +1019,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.immutable, title: 'Awesome Timeline', timelineType: TimelineType.template, @@ -1106,10 +1120,12 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.active, title: 'Awesome Timeline', timelineType: TimelineType.default, 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 1ee529cc77a91..76eb9196e8c5c 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 @@ -108,21 +108,20 @@ const parseString = (params: string) => { } }; -const setTimelineColumn = (col: ColumnHeaderResult) => { - const timelineCols: ColumnHeaderOptions = { - ...col, - columnHeaderType: defaultColumnHeaderType, - id: col.id != null ? col.id : 'unknown', - placeholder: col.placeholder != null ? col.placeholder : undefined, - category: col.category != null ? col.category : undefined, - description: col.description != null ? col.description : undefined, - example: col.example != null ? col.example : undefined, - type: col.type != null ? col.type : undefined, - aggregatable: col.aggregatable != null ? col.aggregatable : undefined, - width: col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH, - }; - return timelineCols; -}; +const setTimelineColumn = (col: ColumnHeaderResult) => + Object.entries(col).reduce( + (acc, [key, value]) => { + if (key !== 'id' && value != null) { + return { ...acc, [key]: value }; + } + return acc; + }, + { + columnHeaderType: defaultColumnHeaderType, + id: col.id != null ? col.id : 'unknown', + width: col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH, + } + ); const setTimelineFilters = (filter: FilterTimelineResult) => ({ $state: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 7772bcede76fc..36e0652c3032a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -2,7 +2,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx index c4c4e0e0c7065..8ec8827ccbed6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx @@ -18,7 +18,7 @@ interface Props { header: ColumnHeaderOptions; isLoading: boolean; onColumnRemoved: OnColumnRemoved; - sort: Sort; + sort: Sort[]; } /** Given a `header`, returns the `SortDirection` applicable to it */ @@ -53,7 +53,7 @@ CloseButton.displayName = 'CloseButton'; export const Actions = React.memo(({ header, onColumnRemoved, sort, isLoading }) => { return ( <> - {sort.columnId === header.id && isLoading ? ( + {sort.some((i) => i.columnId === header.id) && isLoading ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx index 8bf9b6ceb346a..543ffe2798947 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx @@ -26,7 +26,7 @@ interface ColumneHeaderProps { header: ColumnHeaderOptions; isDragging: boolean; onFilterChange?: OnFilterChange; - sort: Sort; + sort: Sort[]; timelineId: string; } @@ -131,6 +131,6 @@ export const ColumnHeader = React.memo( prevProps.timelineId === nextProps.timelineId && prevProps.isDragging === nextProps.isDragging && prevProps.onFilterChange === nextProps.onFilterChange && - prevProps.sort === nextProps.sort && + deepEqual(prevProps.sort, nextProps.sort) && deepEqual(prevProps.header, nextProps.header) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap index 517f537b9a01b..fa9a4e78d88f2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap @@ -14,10 +14,12 @@ exports[`Header renders correctly against snapshot 1`] = ` isResizing={false} onClick={[Function]} sort={ - Object { - "columnId": "@timestamp", - "sortDirection": "desc", - } + Array [ + Object { + "columnId": "@timestamp", + "sortDirection": "desc", + }, + ] } > diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx index 19d0220cd3462..656cf234ea662 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx @@ -14,15 +14,14 @@ import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from import { Sort } from '../../sort'; import { SortIndicator } from '../../sort/sort_indicator'; import { HeaderToolTipContent } from '../header_tooltip_content'; -import { getSortDirection } from './helpers'; - +import { getSortDirection, getSortIndex } from './helpers'; interface HeaderContentProps { children: React.ReactNode; header: ColumnHeaderOptions; isLoading: boolean; isResizing: boolean; onClick: () => void; - sort: Sort; + sort: Sort[]; } const HeaderContentComponent: React.FC = ({ @@ -33,7 +32,7 @@ const HeaderContentComponent: React.FC = ({ onClick, sort, }) => ( - + {header.aggregatable ? ( = ({ ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts index 609f690903bf2..b2ad186ce1b1e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts @@ -11,7 +11,7 @@ import { Sort, SortDirection } from '../../sort'; interface GetNewSortDirectionOnClickParams { clickedHeader: ColumnHeaderOptions; - currentSort: Sort; + currentSort: Sort[]; } /** Given a `header`, returns the `SortDirection` applicable to it */ @@ -19,7 +19,10 @@ export const getNewSortDirectionOnClick = ({ clickedHeader, currentSort, }: GetNewSortDirectionOnClickParams): Direction => - clickedHeader.id === currentSort.columnId ? getNextSortDirection(currentSort) : Direction.desc; + currentSort.reduce( + (acc, item) => (clickedHeader.id === item.columnId ? getNextSortDirection(item) : acc), + Direction.desc + ); /** Given a current sort direction, it returns the next sort direction */ export const getNextSortDirection = (currentSort: Sort): Direction => { @@ -37,8 +40,14 @@ export const getNextSortDirection = (currentSort: Sort): Direction => { interface GetSortDirectionParams { header: ColumnHeaderOptions; - sort: Sort; + sort: Sort[]; } export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection => - header.id === sort.columnId ? sort.sortDirection : 'none'; + sort.reduce( + (acc, item) => (header.id === item.columnId ? item.sortDirection : acc), + 'none' + ); + +export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number => + sort.findIndex((s) => s.columnId === header.id); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx index 3ef9beb89309e..58d40c94ac338 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx @@ -32,10 +32,12 @@ const filteredColumnHeader: ColumnHeaderType = 'text-filter'; describe('Header', () => { const columnHeader = defaultHeaders[0]; - const sort: Sort = { - columnId: columnHeader.id, - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: columnHeader.id, + sortDirection: Direction.desc, + }, + ]; const timelineId = 'fakeId'; test('renders correctly against snapshot', () => { @@ -119,10 +121,12 @@ describe('Header', () => { expect(mockDispatch).toBeCalledWith( timelineActions.updateSort({ id: timelineId, - sort: { - columnId: columnHeader.id, - sortDirection: Direction.asc, // (because the previous state was Direction.desc) - }, + sort: [ + { + columnId: columnHeader.id, + sortDirection: Direction.asc, // (because the previous state was Direction.desc) + }, + ], }) ); }); @@ -158,7 +162,7 @@ describe('Header', () => { ); - wrapper.find('[data-test-subj="header"]').first().simulate('click'); + wrapper.find(`[data-test-subj="header-${columnHeader.id}"]`).first().simulate('click'); expect(mockOnColumnSorted).not.toHaveBeenCalled(); }); @@ -180,14 +184,16 @@ describe('Header', () => { describe('getSortDirection', () => { test('it returns the sort direction when the header id matches the sort column id', () => { - expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort.sortDirection); + expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort[0].sortDirection); }); test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => { - const nonMatching: Sort = { - columnId: 'differentSocks', - sortDirection: Direction.desc, - }; + const nonMatching: Sort[] = [ + { + columnId: 'differentSocks', + sortDirection: Direction.desc, + }, + ]; expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none'); }); @@ -221,10 +227,12 @@ describe('Header', () => { describe('getNewSortDirectionOnClick', () => { test('it returns the expected new sort direction when the header id matches the sort column id', () => { - const sortMatches: Sort = { - columnId: columnHeader.id, - sortDirection: Direction.desc, - }; + const sortMatches: Sort[] = [ + { + columnId: columnHeader.id, + sortDirection: Direction.desc, + }, + ]; expect( getNewSortDirectionOnClick({ @@ -235,10 +243,12 @@ describe('Header', () => { }); test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => { - const sortDoesNotMatch: Sort = { - columnId: 'someOtherColumn', - sortDirection: 'none', - }; + const sortDoesNotMatch: Sort[] = [ + { + columnId: 'someOtherColumn', + sortDirection: 'none', + }, + ]; expect( getNewSortDirectionOnClick({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx index 15d75cc9a4384..192a9c6b0973b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx @@ -21,7 +21,7 @@ import { useManageTimeline } from '../../../../manage_timeline'; interface Props { header: ColumnHeaderOptions; onFilterChange?: OnFilterChange; - sort: Sort; + sort: Sort[]; timelineId: string; } @@ -33,22 +33,39 @@ export const HeaderComponent: React.FC = ({ }) => { const dispatch = useDispatch(); - const onClick = useCallback( - () => - dispatch( - timelineActions.updateSort({ - id: timelineId, - sort: { - columnId: header.id, - sortDirection: getNewSortDirectionOnClick({ - clickedHeader: header, - currentSort: sort, - }), - }, - }) - ), - [dispatch, header, timelineId, sort] - ); + const onColumnSort = useCallback(() => { + const columnId = header.id; + const sortDirection = getNewSortDirectionOnClick({ + clickedHeader: header, + currentSort: sort, + }); + const headerIndex = sort.findIndex((col) => col.columnId === columnId); + let newSort = []; + if (headerIndex === -1) { + newSort = [ + ...sort, + { + columnId, + sortDirection, + }, + ]; + } else { + newSort = [ + ...sort.slice(0, headerIndex), + { + columnId, + sortDirection, + }, + ...sort.slice(headerIndex + 1), + ]; + } + dispatch( + timelineActions.updateSort({ + id: timelineId, + sort: newSort, + }) + ); + }, [dispatch, header, sort, timelineId]); const onColumnRemoved = useCallback( (columnId) => dispatch(timelineActions.removeColumn({ id: timelineId, columnId })), @@ -68,7 +85,7 @@ export const HeaderComponent: React.FC = ({ header={header} isLoading={isLoading} isResizing={false} - onClick={onClick} + onClick={onColumnSort} sort={sort} > { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); +const timelineId = 'test'; describe('ColumnHeaders', () => { const mount = useMountAppended(); describe('rendering', () => { - const sort: Sort = { - columnId: 'fooColumn', - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ]; test('renders correctly against snapshot', () => { const wrapper = shallow( @@ -39,7 +54,7 @@ describe('ColumnHeaders', () => { showEventsSelect={false} showSelectAllCheckbox={false} sort={sort} - timelineId={'test'} + timelineId={timelineId} /> ); @@ -58,7 +73,7 @@ describe('ColumnHeaders', () => { showEventsSelect={false} showSelectAllCheckbox={false} sort={sort} - timelineId={'test'} + timelineId={timelineId} /> ); @@ -78,7 +93,7 @@ describe('ColumnHeaders', () => { showEventsSelect={false} showSelectAllCheckbox={false} sort={sort} - timelineId={'test'} + timelineId={timelineId} /> ); @@ -88,4 +103,145 @@ describe('ColumnHeaders', () => { }); }); }); + + describe('#onColumnsSorted', () => { + let mockSort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + sortDirection: Direction.asc, + }, + ]; + let mockDefaultHeaders = cloneDeep( + defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) + ); + + beforeEach(() => { + mockDefaultHeaders = cloneDeep( + defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) + ); + mockSort = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + sortDirection: Direction.asc, + }, + ]; + }); + + test('Add column `event.category` as desc sorting', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-event.category"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + sortDirection: Direction.asc, + }, + { columnId: 'event.category', sortDirection: Direction.desc }, + ], + }) + ); + }); + + test('Change order of column `@timestamp` from desc to asc without changing index position', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-@timestamp"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.asc, + }, + { columnId: 'host.name', sortDirection: Direction.asc }, + ], + }) + ); + }); + + test('Change order of column `host.name` from asc to desc without changing index position', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-host.name"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + timelineActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + { columnId: 'host.name', sortDirection: Direction.desc }, + ], + }) + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index aeab6a774ca41..66856f3bd6284 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -4,10 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiCheckbox, EuiToolTip } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiCheckbox, + EuiDataGridSorting, + EuiToolTip, + useDataGridColumnSorting, +} from '@elastic/eui'; +import deepEqual from 'fast-deep-equal'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; -import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper'; import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge'; @@ -34,11 +42,18 @@ import { EventsThGroupData, EventsTrHeader, } from '../../styles'; -import { Sort } from '../sort'; +import { Sort, SortDirection } from '../sort'; import { EventsSelect } from './events_select'; import { ColumnHeader } from './column_header'; import * as i18n from './translations'; +import { timelineActions } from '../../../../store/timeline'; + +const SortingColumnsContainer = styled.div` + .euiPopover .euiButtonEmpty .euiButtonContent .euiButtonEmpty__text { + display: none; + } +`; interface Props { actionsColumnWidth: number; @@ -49,7 +64,7 @@ interface Props { onSelectAll: OnSelectAll; showEventsSelect: boolean; showSelectAllCheckbox: boolean; - sort: Sort; + sort: Sort[]; timelineId: string; } @@ -98,6 +113,7 @@ export const ColumnHeadersComponent = ({ sort, timelineId, }: Props) => { + const dispatch = useDispatch(); const [draggingIndex, setDraggingIndex] = useState(null); const { timelineFullScreen, @@ -189,6 +205,48 @@ export const ColumnHeadersComponent = ({ [ColumnHeaderList] ); + const myColumns = useMemo( + () => + columnHeaders.map(({ aggregatable, label, id, type }) => ({ + id, + isSortable: aggregatable, + displayAsText: label, + schema: type, + })), + [columnHeaders] + ); + + const onSortColumns = useCallback( + (cols: EuiDataGridSorting['columns']) => + dispatch( + timelineActions.updateSort({ + id: timelineId, + sort: cols.map(({ id, direction }) => ({ + columnId: id, + sortDirection: direction as SortDirection, + })), + }) + ), + [dispatch, timelineId] + ); + const sortedColumns = useMemo( + () => ({ + onSort: onSortColumns, + columns: sort.map<{ id: string; direction: 'asc' | 'desc' }>( + ({ columnId, sortDirection }) => ({ + id: columnId, + direction: sortDirection as 'asc' | 'desc', + }) + ), + }), + [onSortColumns, sort] + ); + const displayValues = useMemo( + () => columnHeaders.reduce((acc, ch) => ({ ...acc, [ch.id]: ch.label ?? ch.id }), {}), + [columnHeaders] + ); + const ColumnSorting = useDataGridColumnSorting(myColumns, sortedColumns, {}, [], displayValues); + return ( @@ -245,6 +303,13 @@ export const ColumnHeadersComponent = ({ + + + + {ColumnSorting} + + + {showEventsSelect && ( @@ -278,7 +343,7 @@ export const ColumnHeaders = React.memo( prevProps.onSelectAll === nextProps.onSelectAll && prevProps.showEventsSelect === nextProps.showEventsSelect && prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && - prevProps.sort === nextProps.sort && + deepEqual(prevProps.sort, nextProps.sort) && prevProps.timelineId === nextProps.timelineId && deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && deepEqual(prevProps.browserFields, nextProps.browserFields) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts index 1ebfa957b654f..c946182ddfe06 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts @@ -22,6 +22,10 @@ export const FULL_SCREEN = i18n.translate('xpack.securitySolution.timeline.fullS defaultMessage: 'Full screen', }); +export const SORT_FIELDS = i18n.translate('xpack.securitySolution.timeline.sortFieldsButton', { + defaultMessage: 'Sort fields', +}); + export const TYPE = i18n.translate('xpack.securitySolution.timeline.typeTooltip', { defaultMessage: 'Type', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts index 6fddb5403561e..bf70d7bff1ff5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts @@ -7,15 +7,17 @@ /** The minimum (fixed) width of the Actions column */ export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px; +/** Additional column width to include when checkboxes are shown **/ +export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; + /** The (fixed) width of the Actions column */ -export const DEFAULT_ACTIONS_COLUMN_WIDTH = 24 * 4; // px; +export const DEFAULT_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 5; // px; /** * The (fixed) width of the Actions column when the timeline body is used as * an events viewer, which has fewer actions than a regular events viewer */ -export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 24 * 3; // px; -/** Additional column width to include when checkboxes are shown **/ -export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; +export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 4; // px; + /** The default minimum width of a column (when a width for the column type is not specified) */ export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px /** The default minimum width of a column of type `date` */ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index 1d4cea700d003..0dae9a97b6e5b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -117,7 +117,9 @@ export const getEventType = (event: Ecs): Omit => { }; export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => - get(['agent', 'type', 0], ecsData) === 'endpoint' && + (get(['agent', 'type', 0], ecsData) === 'endpoint' || + (get(['agent', 'type', 0], ecsData) === 'winlogbeat' && + get(['event', 'module', 0], ecsData) === 'sysmon')) && get(['process', 'entity_id'], ecsData)?.length === 1 && get(['process', 'entity_id', 0], ecsData) !== ''; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index fc9967bdeff98..704af61b4a12f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -16,13 +16,14 @@ import { TestProviders } from '../../../../common/mock/test_providers'; import { BodyComponent, StatefulBodyProps } from '.'; import { Sort } from './sort'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; -import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../styles'; import { timelineActions } from '../../../store/timeline'; -const mockSort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, -}; +const mockSort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, +]; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -130,20 +131,6 @@ describe('Body', () => { }); }); }, 20000); - - test(`it add attribute data-timeline-id in ${SELECTOR_TIMELINE_BODY_CLASS_NAME}`, () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_BODY_CLASS_NAME}`) - .first() - .exists() - ).toEqual(true); - }); }); describe('action on event', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 45641a34f2cf4..ea397b67c31cc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -33,7 +33,7 @@ interface OwnProps { data: TimelineItem[]; id: string; isEventViewer?: boolean; - sort: Sort; + sort: Sort[]; refetch: inputsModel.Refetch; onRuleChange?: () => void; } @@ -144,7 +144,7 @@ export const BodyComponent = React.memo( return ( <> - + ); + } else if (fieldType === GEO_FIELD_TYPE) { + return <>{value}; } else if (fieldType === DATE_FIELD_TYPE) { return ( + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx index dcaedb90e7252..6593abf71e368 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx @@ -15,12 +15,12 @@ import { getDirection, SortIndicator } from './sort_indicator'; describe('SortIndicator', () => { describe('rendering', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); test('it renders the expected sort indicator when direction is ascending', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'sortUp' @@ -28,7 +28,7 @@ describe('SortIndicator', () => { }); test('it renders the expected sort indicator when direction is descending', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'sortDown' @@ -36,7 +36,7 @@ describe('SortIndicator', () => { }); test('it renders the expected sort indicator when direction is `none`', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'empty' @@ -60,7 +60,7 @@ describe('SortIndicator', () => { describe('sort indicator tooltip', () => { test('it returns the expected tooltip when the direction is ascending', () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content @@ -68,7 +68,7 @@ describe('SortIndicator', () => { }); test('it returns the expected tooltip when the direction is descending', () => { - const wrapper = mount(); + const wrapper = mount(); expect( wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content @@ -76,7 +76,7 @@ describe('SortIndicator', () => { }); test('it does NOT render a tooltip when sort direction is `none`', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx index 8b842dfa2197e..518103e8cb643 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { Direction } from '../../../../../graphql/types'; import * as i18n from '../translations'; +import { SortNumber } from './sort_number'; import { SortDirection } from '.'; @@ -35,10 +36,11 @@ export const getDirection = (sortDirection: SortDirection): SortDirectionIndicat interface Props { sortDirection: SortDirection; + sortNumber: number; } /** Renders a sort indicator */ -export const SortIndicator = React.memo(({ sortDirection }) => { +export const SortIndicator = React.memo(({ sortDirection, sortNumber }) => { const direction = getDirection(sortDirection); if (direction != null) { @@ -51,7 +53,10 @@ export const SortIndicator = React.memo(({ sortDirection }) => { } data-test-subj="sort-indicator-tooltip" > - + <> + + + ); } else { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.tsx new file mode 100644 index 0000000000000..48dd70a16e70a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_number.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 { EuiIcon, EuiNotificationBadge } from '@elastic/eui'; +import React from 'react'; + +interface Props { + sortNumber: number; +} + +export const SortNumber = React.memo(({ sortNumber }) => { + if (sortNumber >= 0) { + return ( + + {sortNumber + 1} + + ); + } else { + return ; + } +}); + +SortNumber.displayName = 'SortNumber'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts index 54755fbc84277..11bc3da8c05bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts @@ -38,6 +38,10 @@ export type OnFilterChange = (filter: { columnId: ColumnId; filter: string }) => /** Invoked when a column is sorted */ export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void; +export type OnColumnsSorted = ( + sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }> +) => void; + export type OnColumnRemoved = (columnId: ColumnId) => void; export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index 085a9bf8cba3f..59a7b936dfbac 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -20,6 +20,7 @@ import { import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index'; import { useTimelineEvents } from '../../containers/index'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from './styles'; jest.mock('../../containers/index', () => ({ useTimelineEvents: jest.fn(), @@ -56,7 +57,7 @@ jest.mock('../../../common/containers/sourcerer', () => { }); describe('StatefulTimeline', () => { const props: StatefulTimelineOwnProps = { - timelineId: 'id', + timelineId: 'timeline-test', }; beforeEach(() => { @@ -71,4 +72,18 @@ describe('StatefulTimeline', () => { ); expect(wrapper.find('[data-test-subj="timeline"]')).toBeTruthy(); }); + + test(`it add attribute data-timeline-id in ${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`, () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`) + .first() + .exists() + ).toEqual(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 37145b9348ac1..4e6bca7fd9625 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -21,13 +21,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/h import { activeTimeline } from '../../containers/active_timeline_context'; import * as i18n from './translations'; import { TabsContent } from './tabs_content'; - -const TimelineContainer = styled.div` - height: 100%; - display: flex; - flex-direction: column; - position: relative; -`; +import { TimelineContainer } from './styles'; const TimelineTemplateBadge = styled.div` background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; @@ -78,7 +72,7 @@ const StatefulTimelineComponent: React.FC = ({ timelineId }) => { }, []); return ( - + {timelineType === TimelineType.template && ( {i18n.TIMELINE_TEMPLATE} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap index c726e92455f25..c9355797193a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap @@ -276,10 +276,12 @@ In other use cases the message field can be used to concatenate different values showCallOutUnauthorizedMsg={false} showEventDetails={false} sort={ - Object { - "columnId": "@timestamp", - "sortDirection": "desc", - } + Array [ + Object { + "columnId": "@timestamp", + "sortDirection": "desc", + }, + ] } start="2018-03-23T18:49:23.132Z" status="active" diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index 4019f46b8c07b..7e60461a01574 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -64,10 +64,12 @@ jest.mock('../../../../common/lib/kibana', () => { describe('Timeline', () => { let props = {} as QueryTabContentComponentProps; - const sort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ]; const startDate = '2018-03-23T18:49:23.132Z'; const endDate = '2018-03-24T03:33:52.253Z'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 8186ee8b77628..69a7299b9833d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -214,11 +214,12 @@ export const QueryTabContentComponent: React.FC = ({ }, [columns]); const timelineQuerySortField = useMemo( - () => ({ - field: sort.columnId, - direction: sort.sortDirection as Direction, - }), - [sort.columnId, sort.sortDirection] + () => + sort.map(({ columnId, sortDirection }) => ({ + field: columnId, + direction: sortDirection as Direction, + })), + [sort] ); const { initializeTimeline, setIsTimelineLoading } = useManageTimeline(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 9f9940203960c..ef7c821bd652d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -11,6 +11,19 @@ import styled, { createGlobalStyle } from 'styled-components'; import { TimelineEventsType } from '../../../../common/types/timeline'; import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../../../common/components/drag_and_drop/helpers'; +/** + * TIMELINE BODY + */ +export const SELECTOR_TIMELINE_GLOBAL_CONTAINER = 'securitySolutionTimeline__container'; +export const TimelineContainer = styled.div.attrs(({ className = '' }) => ({ + className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, +}))` + height: 100%; + display: flex; + flex-direction: column; + position: relative; +`; + /** * TIMELINE BODY */ @@ -99,6 +112,9 @@ export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({ min-width: 0; padding-left: ${({ isEventViewer }) => !isEventViewer ? '4px;' : '0;'}; // match timeline event border + button { + color: ${({ theme }) => theme.eui.euiColorPrimary}; + } `; export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts index a439699d27f6d..7e2a6fa1c15cf 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts @@ -73,10 +73,12 @@ const timelineData = { end: 1591084965409, }, savedQueryId: null, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], status: TimelineStatus.active, }; const mockPatchTimelineResponse = { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index 8f1644550d147..ebc86b3c5cf5e 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -85,6 +85,9 @@ export const useTimelineEventsDetails = ({ } }, error: () => { + if (!didCancel) { + setLoading(false); + } notifications.toasts.addDanger('Failed to run search'); }, }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index a168e814208e7..3baab2024558f 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -56,7 +56,7 @@ export interface UseTimelineEventsProps { fields: string[]; indexNames: string[]; limit: number; - sort: SortField; + sort: SortField[]; startDate: string; timerangeKind?: 'absolute' | 'relative'; } @@ -65,10 +65,12 @@ const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] => timelineEdges.map((e: TimelineEdges) => e.node); const ID = 'timelineEventsQuery'; -export const initSortDefault = { - field: '@timestamp', - direction: Direction.asc, -}; +export const initSortDefault = [ + { + field: '@timestamp', + direction: Direction.asc, + }, +]; function usePreviousRequest(value: TimelineEventsAllRequestOptions | null) { const ref = useRef(value); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx index 1a09868da7771..604767bcde26c 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx @@ -32,7 +32,12 @@ export const getTimelinesInStorageByIds = (storage: Storage, timelineIds: Timeli return { ...acc, - [timelineId]: timelineModel, + [timelineId]: { + ...timelineModel, + ...(timelineModel.sort != null && !Array.isArray(timelineModel.sort) + ? { sort: [timelineModel.sort] } + : {}), + }, }; }, {} as { [K in TimelineIdLiteral]: TimelineModel }); }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts index fa0ecb349f9c8..9e34d3470d296 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts @@ -138,10 +138,7 @@ export const oneTimelineQuery = gql` templateTimelineId templateTimelineVersion savedQueryId - sort { - columnId - sortDirection - } + sort created createdBy updated diff --git a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts index 12d3e6bfd7172..e255ac5bdda5b 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts @@ -102,10 +102,7 @@ export const persistTimelineMutation = gql` end } savedQueryId - sort { - columnId - sortDirection - } + sort created createdBy updated diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index b8dfa698a9307..479c289cdd21d 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -72,7 +72,7 @@ export interface TimelineInput { filterQuery: SerializedFilterQuery | null; }; show?: boolean; - sort?: Sort; + sort?: Sort[]; showCheckboxes?: boolean; timelineType?: TimelineTypeLiteral; templateTimelineId?: string | null; @@ -216,7 +216,7 @@ export const updateRange = actionCreator<{ id: string; start: string; end: strin 'UPDATE_RANGE' ); -export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT'); +export const updateSort = actionCreator<{ id: string; sort: Sort[] }>('UPDATE_SORT'); export const updateAutoSaveMsg = actionCreator<{ timelineId: string | null; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 84551de9ec628..211bba3cc47d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -52,10 +52,12 @@ export const timelineDefaults: SubsetTimelineModel & Pick { selectedEventIds: {}, show: true, showCheckboxes: false, - sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + sort: [{ columnId: '@timestamp', sortDirection: Direction.desc }], status: TimelineStatus.active, version: 'WzM4LDFd', id: '11169110-fc22-11e9-8ca9-072f15ce2685', @@ -286,10 +286,12 @@ describe('Epic Timeline', () => { }, }, savedQueryId: 'my endgame timeline query', - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: 'desc', + }, + ], templateTimelineId: null, templateTimelineVersion: null, timelineType: TimelineType.default, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index a2bccaddb309e..5fcbcf434d3ee 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -58,10 +58,12 @@ describe('epicLocalStorage', () => { ); let props = {} as QueryTabContentComponentProps; - const sort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, - }; + const sort: Sort[] = [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ]; const startDate = '2018-03-23T18:49:23.132Z'; const endDate = '2018-03-24T03:33:52.253Z'; @@ -159,10 +161,12 @@ describe('epicLocalStorage', () => { store.dispatch( updateSort({ id: 'test', - sort: { - columnId: 'event.severity', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: 'event.severity', + sortDirection: Direction.desc, + }, + ], }) ); await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 1122b7a94e0e0..f9f4622c9d63c 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -179,7 +179,7 @@ interface AddNewTimelineParams { filterQuery: SerializedFilterQuery | null; }; show?: boolean; - sort?: Sort; + sort?: Sort[]; showCheckboxes?: boolean; timelineById: TimelineById; timelineType: TimelineTypeLiteral; @@ -762,7 +762,7 @@ export const updateTimelineRange = ({ interface UpdateTimelineSortParams { id: string; - sort: Sort; + sort: Sort[]; timelineById: TimelineById; } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index e4d1a6b512689..9c71fabfffac5 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -124,7 +124,7 @@ export interface TimelineModel { /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ showCheckboxes: boolean; /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ - sort: Sort; + sort: Sort[]; /** status: active | draft */ status: TimelineStatus; /** updated saved object timestamp */ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 2ca34742affef..59d5800271b8a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -101,10 +101,12 @@ const basicTimeline: TimelineModel = { selectedEventIds: {}, show: true, showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + ], status: TimelineStatus.active, templateTimelineId: null, templateTimelineVersion: null, @@ -953,10 +955,12 @@ describe('Timeline', () => { beforeAll(() => { update = updateTimelineSort({ id: 'foo', - sort: { - columnId: 'some column', - sortDirection: Direction.desc, - }, + sort: [ + { + columnId: 'some column', + sortDirection: Direction.desc, + }, + ], timelineById: timelineByIdMock, }); }); @@ -964,8 +968,8 @@ describe('Timeline', () => { expect(update).not.toBe(timelineByIdMock); }); - test('should update the timeline range', () => { - expect(update.foo.sort).toEqual({ columnId: 'some column', sortDirection: Direction.desc }); + test('should update the sort attribute', () => { + expect(update.foo.sort).toEqual([{ columnId: 'some column', sortDirection: Direction.desc }]); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts index c731692e6fb89..6d4168d744fca 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts @@ -19,7 +19,7 @@ interface SupportedSchema { /** * A constraint to search for in the documented returned by Elasticsearch */ - constraint: { field: string; value: string }; + constraints: Array<{ field: string; value: string }>; /** * Schema to return to the frontend so that it can be passed in to call to the /tree API @@ -34,10 +34,12 @@ interface SupportedSchema { const supportedSchemas: SupportedSchema[] = [ { name: 'endpoint', - constraint: { - field: 'agent.type', - value: 'endpoint', - }, + constraints: [ + { + field: 'agent.type', + value: 'endpoint', + }, + ], schema: { id: 'process.entity_id', parent: 'process.parent.entity_id', @@ -47,10 +49,16 @@ const supportedSchemas: SupportedSchema[] = [ }, { name: 'winlogbeat', - constraint: { - field: 'agent.type', - value: 'winlogbeat', - }, + constraints: [ + { + field: 'agent.type', + value: 'winlogbeat', + }, + { + field: 'event.module', + value: 'sysmon', + }, + ], schema: { id: 'process.entity_id', parent: 'process.parent.entity_id', @@ -104,14 +112,17 @@ export function handleEntities(): RequestHandler { - const kqlQuery: JsonObject[] = []; - if (kql) { - kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); - } + async search( + client: IScopedClusterClient, + filter: string | undefined + ): Promise { + const parsedFilters = EventsQuery.buildFilters(filter); const response: ApiResponse< SearchResponse - > = await client.asCurrentUser.search(this.buildSearch(kqlQuery)); + > = await client.asCurrentUser.search(this.buildSearch(parsedFilters)); return response.body.hits.hits.map((hit) => hit._source); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts index 3baf3a8667529..63cd3b5d694af 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts @@ -8,12 +8,12 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types'; import { JsonObject, JsonValue } from '../../../../../../../../../src/plugins/kibana_utils/common'; -import { NodeID, Timerange, docValueFields } from '../utils/index'; +import { NodeID, TimeRange, docValueFields } from '../utils/index'; interface DescendantsParams { schema: ResolverSchema; indexPatterns: string | string[]; - timerange: Timerange; + timeRange: TimeRange; } /** @@ -22,13 +22,13 @@ interface DescendantsParams { export class DescendantsQuery { private readonly schema: ResolverSchema; private readonly indexPatterns: string | string[]; - private readonly timerange: Timerange; + private readonly timeRange: TimeRange; private readonly docValueFields: JsonValue[]; - constructor({ schema, indexPatterns, timerange }: DescendantsParams) { + constructor({ schema, indexPatterns, timeRange }: DescendantsParams) { this.docValueFields = docValueFields(schema); this.schema = schema; this.indexPatterns = indexPatterns; - this.timerange = timerange; + this.timeRange = timeRange; } private query(nodes: NodeID[], size: number): JsonObject { @@ -46,8 +46,8 @@ export class DescendantsQuery { { range: { '@timestamp': { - gte: this.timerange.from, - lte: this.timerange.to, + gte: this.timeRange.from, + lte: this.timeRange.to, format: 'strict_date_optional_time', }, }, @@ -126,8 +126,8 @@ export class DescendantsQuery { { range: { '@timestamp': { - gte: this.timerange.from, - lte: this.timerange.to, + gte: this.timeRange.from, + lte: this.timeRange.to, format: 'strict_date_optional_time', }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts index 5253806be66ba..150b07c63ce2f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts @@ -8,12 +8,12 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types'; import { JsonObject, JsonValue } from '../../../../../../../../../src/plugins/kibana_utils/common'; -import { NodeID, Timerange, docValueFields } from '../utils/index'; +import { NodeID, TimeRange, docValueFields } from '../utils/index'; interface LifecycleParams { schema: ResolverSchema; indexPatterns: string | string[]; - timerange: Timerange; + timeRange: TimeRange; } /** @@ -22,13 +22,13 @@ interface LifecycleParams { export class LifecycleQuery { private readonly schema: ResolverSchema; private readonly indexPatterns: string | string[]; - private readonly timerange: Timerange; + private readonly timeRange: TimeRange; private readonly docValueFields: JsonValue[]; - constructor({ schema, indexPatterns, timerange }: LifecycleParams) { + constructor({ schema, indexPatterns, timeRange }: LifecycleParams) { this.docValueFields = docValueFields(schema); this.schema = schema; this.indexPatterns = indexPatterns; - this.timerange = timerange; + this.timeRange = timeRange; } private query(nodes: NodeID[]): JsonObject { @@ -46,8 +46,8 @@ export class LifecycleQuery { { range: { '@timestamp': { - gte: this.timerange.from, - lte: this.timerange.to, + gte: this.timeRange.from, + lte: this.timeRange.to, format: 'strict_date_optional_time', }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts index 117cc3647dd0e..22d2c600feb01 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts @@ -8,7 +8,7 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; import { JsonObject } from '../../../../../../../../../src/plugins/kibana_utils/common'; import { EventStats, ResolverSchema } from '../../../../../../common/endpoint/types'; -import { NodeID, Timerange } from '../utils/index'; +import { NodeID, TimeRange } from '../utils/index'; interface AggBucket { key: string; @@ -28,7 +28,7 @@ interface CategoriesAgg extends AggBucket { interface StatsParams { schema: ResolverSchema; indexPatterns: string | string[]; - timerange: Timerange; + timeRange: TimeRange; } /** @@ -37,11 +37,11 @@ interface StatsParams { export class StatsQuery { private readonly schema: ResolverSchema; private readonly indexPatterns: string | string[]; - private readonly timerange: Timerange; - constructor({ schema, indexPatterns, timerange }: StatsParams) { + private readonly timeRange: TimeRange; + constructor({ schema, indexPatterns, timeRange }: StatsParams) { this.schema = schema; this.indexPatterns = indexPatterns; - this.timerange = timerange; + this.timeRange = timeRange; } private query(nodes: NodeID[]): JsonObject { @@ -53,8 +53,8 @@ export class StatsQuery { { range: { '@timestamp': { - gte: this.timerange.from, - lte: this.timerange.to, + gte: this.timeRange.from, + lte: this.timeRange.to, format: 'strict_date_optional_time', }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.test.ts index d5e0af9dea239..796ed60ddbbc3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.test.ts @@ -80,7 +80,7 @@ describe('fetcher test', () => { descendantLevels: 1, descendants: 5, ancestors: 0, - timerange: { + timeRange: { from: '', to: '', }, @@ -100,7 +100,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 0, - timerange: { + timeRange: { from: '', to: '', }, @@ -163,7 +163,7 @@ describe('fetcher test', () => { descendantLevels: 2, descendants: 5, ancestors: 0, - timerange: { + timeRange: { from: '', to: '', }, @@ -188,7 +188,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 5, - timerange: { + timeRange: { from: '', to: '', }, @@ -211,7 +211,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 0, - timerange: { + timeRange: { from: '', to: '', }, @@ -249,7 +249,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 2, - timerange: { + timeRange: { from: '', to: '', }, @@ -292,7 +292,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 2, - timerange: { + timeRange: { from: '', to: '', }, @@ -342,7 +342,7 @@ describe('fetcher test', () => { descendantLevels: 0, descendants: 0, ancestors: 3, - timerange: { + timeRange: { from: '', to: '', }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts index 356357082d6ee..2ff231892a593 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/fetch.ts @@ -27,7 +27,7 @@ export interface TreeOptions { descendantLevels: number; descendants: number; ancestors: number; - timerange: { + timeRange: { from: string; to: string; }; @@ -76,7 +76,7 @@ export class Fetcher { const query = new StatsQuery({ indexPatterns: options.indexPatterns, schema: options.schema, - timerange: options.timerange, + timeRange: options.timeRange, }); const eventStats = await query.search(this.client, statsIDs); @@ -136,7 +136,7 @@ export class Fetcher { const query = new LifecycleQuery({ schema: options.schema, indexPatterns: options.indexPatterns, - timerange: options.timerange, + timeRange: options.timeRange, }); let nodes = options.nodes; @@ -182,7 +182,7 @@ export class Fetcher { const query = new DescendantsQuery({ schema: options.schema, indexPatterns: options.indexPatterns, - timerange: options.timerange, + timeRange: options.timeRange, }); let nodes: NodeID[] = options.nodes; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/index.ts index be08b4390a69c..c00e90a386fb6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/utils/index.ts @@ -9,7 +9,7 @@ import { ResolverSchema } from '../../../../../../common/endpoint/types'; /** * Represents a time range filter */ -export interface Timerange { +export interface TimeRange { from: string; to: string; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts index 5bc911fb075b5..00aab683bf010 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts @@ -49,18 +49,18 @@ export class AncestryQueryHandler implements QueryHandler private toMapOfNodes(results: SafeResolverEvent[]) { return results.reduce( (nodes: Map, event: SafeResolverEvent) => { - const nodeId = entityIDSafeVersion(event); - if (!nodeId) { + const nodeID = entityIDSafeVersion(event); + if (!nodeID) { return nodes; } - let node = nodes.get(nodeId); + let node = nodes.get(nodeID); if (!node) { - node = createLifecycle(nodeId, []); + node = createLifecycle(nodeID, []); } node.lifecycle.push(event); - return nodes.set(nodeId, node); + return nodes.set(nodeID, node); }, new Map() ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index 1121e27e6e7bc..7476d1b59bf54 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -140,9 +140,7 @@ export const createEntryNested = (field: string, entries: NestedEntriesArray): E return { field, entries, type: 'nested' }; }; -export const conditionEntriesToEntries = ( - conditionEntries: Array> -): EntriesArray => { +export const conditionEntriesToEntries = (conditionEntries: ConditionEntry[]): EntriesArray => { return conditionEntries.map((conditionEntry) => { if (conditionEntry.field === ConditionEntryField.HASH) { return createEntryMatch( diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 2358e78b044ed..ca6c57f025faf 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -174,7 +174,7 @@ export const timelineSchema = gql` timelineType: TimelineType dateRange: DateRangePickerInput savedQueryId: String - sort: SortTimelineInput + sort: [SortTimelineInput!] status: TimelineStatus } @@ -238,10 +238,6 @@ export const timelineSchema = gql` ${favoriteTimeline} } - type SortTimelineResult { - ${sortTimeline} - } - type FilterMetaTimelineResult { ${filtersMetaTimeline} } @@ -277,7 +273,7 @@ export const timelineSchema = gql` pinnedEventsSaveObject: [PinnedEvent!] savedQueryId: String savedObjectId: String! - sort: SortTimelineResult + sort: ToAny status: TimelineStatus title: String templateTimelineId: String diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index bda0fed494a6f..3ea964c0ee01f 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -105,7 +105,7 @@ export interface TimelineInput { savedQueryId?: Maybe; - sort?: Maybe; + sort?: Maybe; status?: Maybe; } @@ -634,7 +634,7 @@ export interface TimelineResult { savedObjectId: string; - sort?: Maybe; + sort?: Maybe; status?: Maybe; @@ -777,12 +777,6 @@ export interface KueryFilterQueryResult { expression?: Maybe; } -export interface SortTimelineResult { - columnId?: Maybe; - - sortDirection?: Maybe; -} - export interface ResponseTimelines { timeline: (Maybe)[]; @@ -2336,7 +2330,6 @@ export namespace AgentFieldsResolvers { > = Resolver; } - export namespace CloudFieldsResolvers { export interface Resolvers { instance?: InstanceResolver, TypeParent, TContext>; @@ -2665,7 +2658,7 @@ export namespace TimelineResultResolvers { savedObjectId?: SavedObjectIdResolver; - sort?: SortResolver, TypeParent, TContext>; + sort?: SortResolver, TypeParent, TContext>; status?: StatusResolver, TypeParent, TContext>; @@ -2785,7 +2778,7 @@ export namespace TimelineResultResolvers { TContext = SiemContext > = Resolver; export type SortResolver< - R = Maybe, + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; @@ -3245,25 +3238,6 @@ export namespace KueryFilterQueryResultResolvers { > = Resolver; } -export namespace SortTimelineResultResolvers { - export interface Resolvers { - columnId?: ColumnIdResolver, TypeParent, TContext>; - - sortDirection?: SortDirectionResolver, TypeParent, TContext>; - } - - export type ColumnIdResolver< - R = Maybe, - Parent = SortTimelineResult, - TContext = SiemContext - > = Resolver; - export type SortDirectionResolver< - R = Maybe, - Parent = SortTimelineResult, - TContext = SiemContext - > = Resolver; -} - export namespace ResponseTimelinesResolvers { export interface Resolvers { timeline?: TimelineResolver<(Maybe)[], TypeParent, TContext>; @@ -6091,7 +6065,6 @@ export type IResolvers = { SerializedFilterQueryResult?: SerializedFilterQueryResultResolvers.Resolvers; SerializedKueryQueryResult?: SerializedKueryQueryResultResolvers.Resolvers; KueryFilterQueryResult?: KueryFilterQueryResultResolvers.Resolvers; - SortTimelineResult?: SortTimelineResultResolvers.Resolvers; ResponseTimelines?: ResponseTimelinesResolvers.Resolvers; Mutation?: MutationResolvers.Resolvers; ResponseNote?: ResponseNoteResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 73e4845924acf..38ac6372fdb9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -129,10 +129,11 @@ export const getDeleteAsPostBulkRequest = () => body: [{ rule_id: 'rule-1' }], }); -export const getPrivilegeRequest = () => +export const getPrivilegeRequest = (options: { auth?: { isAuthenticated: boolean } } = {}) => requestMock.create({ method: 'get', path: DETECTION_ENGINE_PRIVILEGES_URL, + ...options, }); export const addPrepackagedRulesRequest = () => diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index cb4ec99748e47..945be0c584134 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { securityMock } from '../../../../../../security/server/mocks'; import { readPrivilegesRoute } from './read_privileges_route'; import { serverMock, requestContextMock } from '../__mocks__'; import { getPrivilegeRequest, getMockPrivilegesResult } from '../__mocks__/request_responses'; @@ -12,26 +11,29 @@ import { getPrivilegeRequest, getMockPrivilegesResult } from '../__mocks__/reque describe('read_privileges route', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let mockSecurity: ReturnType; beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - mockSecurity = securityMock.createSetup(); - mockSecurity.authc.isAuthenticated.mockReturnValue(false); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivilegesResult()); - readPrivilegesRoute(server.router, mockSecurity, false); + readPrivilegesRoute(server.router, false); }); describe('normal status codes', () => { test('returns 200 when doing a normal request', async () => { - const response = await server.inject(getPrivilegeRequest(), context); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: false } }), + context + ); expect(response.status).toEqual(200); }); test('returns the payload when doing a normal request', async () => { - const response = await server.inject(getPrivilegeRequest(), context); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: false } }), + context + ); const expectedBody = { ...getMockPrivilegesResult(), is_authenticated: false, @@ -42,14 +44,16 @@ describe('read_privileges route', () => { }); test('is authenticated when security says so', async () => { - mockSecurity.authc.isAuthenticated.mockReturnValue(true); const expectedBody = { ...getMockPrivilegesResult(), is_authenticated: true, has_encryption_key: true, }; - const response = await server.inject(getPrivilegeRequest(), context); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: true } }), + context + ); expect(response.status).toEqual(200); expect(response.body).toEqual(expectedBody); }); @@ -58,38 +62,22 @@ describe('read_privileges route', () => { clients.clusterClient.callAsCurrentUser.mockImplementation(() => { throw new Error('Test error'); }); - const response = await server.inject(getPrivilegeRequest(), context); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: false } }), + context + ); expect(response.status).toEqual(500); expect(response.body).toEqual({ message: 'Test error', status_code: 500 }); }); it('returns 404 if siem client is unavailable', async () => { const { securitySolution, ...contextWithoutSecuritySolution } = context; - const response = await server.inject(getPrivilegeRequest(), contextWithoutSecuritySolution); + const response = await server.inject( + getPrivilegeRequest({ auth: { isAuthenticated: false } }), + contextWithoutSecuritySolution + ); expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); }); - - describe('when security plugin is disabled', () => { - beforeEach(() => { - server = serverMock.create(); - ({ clients, context } = requestContextMock.createTools()); - - clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivilegesResult()); - readPrivilegesRoute(server.router, undefined, false); - }); - - it('returns unauthenticated', async () => { - const expectedBody = { - ...getMockPrivilegesResult(), - is_authenticated: false, - has_encryption_key: true, - }; - - const response = await server.inject(getPrivilegeRequest(), context); - expect(response.status).toEqual(200); - expect(response.body).toEqual(expectedBody); - }); - }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 715a5be7462d1..174aa4911ba1e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -8,15 +8,10 @@ import { merge } from 'lodash/fp'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; -import { SetupPlugins } from '../../../../plugin'; import { buildSiemResponse, transformError } from '../utils'; import { readPrivileges } from '../../privileges/read_privileges'; -export const readPrivilegesRoute = ( - router: IRouter, - security: SetupPlugins['security'], - usingEphemeralEncryptionKey: boolean -) => { +export const readPrivilegesRoute = (router: IRouter, usingEphemeralEncryptionKey: boolean) => { router.get( { path: DETECTION_ENGINE_PRIVILEGES_URL, @@ -39,7 +34,7 @@ export const readPrivilegesRoute = ( const index = siemClient.getSignalsIndex(); const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index); const privileges = merge(clusterPrivileges, { - is_authenticated: security?.authc.isAuthenticated(request) ?? false, + is_authenticated: request.auth.isAuthenticated ?? false, has_encryption_key: !usingEphemeralEncryptionKey, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts index a68e534a2d4ea..b2074ad20b674 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -28,7 +28,7 @@ export const findRulesRoute = (router: IRouter) => { ), }, options: { - tags: ['access'], + tags: ['access:securitySolution'], }, }, async (context, request, response) => { diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts index ec801f6c49ae7..c8bf6790ae9b2 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts @@ -6,8 +6,8 @@ import { RequestParams } from '@elastic/elasticsearch'; +import { buildExceptionFilter } from '../../../common/detection_engine/build_exceptions_filter'; import { ExceptionListItemSchema } from '../../../../lists/common'; -import { buildExceptionFilter } from '../../../common/detection_engine/get_query_filter'; import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; import { SearchResponse } from '../types'; @@ -54,12 +54,6 @@ export const getAnomalies = async ( ], must_not: buildExceptionFilter({ lists: params.exceptionItems, - config: { - allowLeadingWildcards: true, - queryStringOptions: { analyze_wildcard: true }, - ignoreFilterIfFieldNotInIndex: false, - dateFormatTZ: 'Zulu', - }, excludeExceptions: true, chunkSize: 1024, })?.query, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index f888675b60410..271e53d4e6c9b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -60,6 +60,12 @@ export const convertSavedObjectToSavedTimeline = (savedObject: unknown): Timelin savedTimeline.attributes.timelineType, savedTimeline.attributes.status ), + sort: + savedTimeline.attributes.sort != null + ? Array.isArray(savedTimeline.attributes.sort) + ? savedTimeline.attributes.sort + : [savedTimeline.attributes.sort] + : [], }; return { savedObjectId: savedTimeline.id, diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 000bd875930f9..3467d0bb66860 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -93,5 +93,5 @@ export const initRoutes = ( readTagsRoute(router); // Privileges API to get the generic user privileges - readPrivilegesRoute(router, security, usingEphemeralEncryptionKey); + readPrivilegesRoute(router, usingEphemeralEncryptionKey); }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts index 1aba6660677cd..9fd371c6f1cca 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts @@ -7,5 +7,37 @@ export const toArray = (value: T | T[] | null): T[] => Array.isArray(value) ? value : value == null ? [] : [value]; -export const toStringArray = (value: T | T[] | null): T[] | string[] => - Array.isArray(value) ? value : value == null ? [] : [`${value}`]; +export const toStringArray = (value: T | T[] | null): string[] => { + if (Array.isArray(value)) { + return value.reduce((acc, v) => { + if (v != null) { + switch (typeof v) { + case 'number': + case 'boolean': + return [...acc, v.toString()]; + case 'object': + try { + return [...acc, JSON.stringify(value)]; + } catch { + return [...acc, 'Invalid Object']; + } + case 'string': + return [...acc, v]; + default: + return [...acc, `${v}`]; + } + } + return acc; + }, []); + } else if (value == null) { + return []; + } else if (!Array.isArray(value) && typeof value === 'object') { + try { + return [JSON.stringify(value)]; + } catch { + return ['Invalid Object']; + } + } else { + return [`${value}`]; + } +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts new file mode 100644 index 0000000000000..b62ddc00f2e30 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -0,0 +1,123 @@ +/* + * 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 { EventHit } from '../../../../../../common/search_strategy'; +import { TIMELINE_EVENTS_FIELDS } from './constants'; +import { formatTimelineData } from './helpers'; + +describe('#formatTimelineData', () => { + it('happy path', () => { + const response: EventHit = { + _index: 'auditbeat-7.8.0-2020.11.05-000003', + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _score: 0, + _type: '', + fields: { + 'event.category': ['process'], + 'process.ppid': [3977], + 'user.name': ['jenkins'], + 'process.args': ['go', 'vet', './...'], + message: ['Process go (PID: 4313) by user jenkins STARTED'], + 'process.pid': [4313], + 'process.working_directory': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], + 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + 'process.name': ['go'], + 'event.action': ['process_started'], + 'agent.type': ['auditbeat'], + '@timestamp': ['2020-11-17T14:48:08.922Z'], + 'event.module': ['system'], + 'event.type': ['start'], + 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + 'host.os.family': ['debian'], + 'event.kind': ['event'], + 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], + 'event.dataset': ['process'], + 'process.executable': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + _source: {}, + sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], + aggregations: {}, + }; + + expect( + formatTimelineData( + ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], + TIMELINE_EVENTS_FIELDS, + response + ) + ).toEqual({ + cursor: { + tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239', + value: '1605624488922', + }, + node: { + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _index: 'auditbeat-7.8.0-2020.11.05-000003', + data: [ + { + field: '@timestamp', + value: ['2020-11-17T14:48:08.922Z'], + }, + { + field: 'host.name', + value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + ], + ecs: { + '@timestamp': ['2020-11-17T14:48:08.922Z'], + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _index: 'auditbeat-7.8.0-2020.11.05-000003', + agent: { + type: ['auditbeat'], + }, + event: { + action: ['process_started'], + category: ['process'], + dataset: ['process'], + kind: ['event'], + module: ['system'], + type: ['start'], + }, + host: { + id: ['e59991e835905c65ed3e455b33e13bd6'], + ip: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + name: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + os: { + family: ['debian'], + }, + }, + message: ['Process go (PID: 4313) by user jenkins STARTED'], + process: { + args: ['go', 'vet', './...'], + entity_id: ['Z59cIkAAIw8ZoK0H'], + executable: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + hash: { + sha1: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + }, + name: ['go'], + pid: ['4313'], + ppid: ['3977'], + working_directory: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + timestamp: '2020-11-17T14:48:08.922Z', + user: { + name: ['jenkins'], + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index 8e2bfb5426610..a9aee2175b31d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -7,6 +7,7 @@ import { get, has, merge, uniq } from 'lodash/fp'; import { EventHit, TimelineEdges } from '../../../../../../common/search_strategy'; import { toStringArray } from '../../../../helpers/to_array'; +import { formatGeoLocation, isGeoField } from '../details/helpers'; export const formatTimelineData = ( dataFields: readonly string[], @@ -18,7 +19,7 @@ export const formatTimelineData = ( flattenedFields.node._id = hit._id; flattenedFields.node._index = hit._index; flattenedFields.node.ecs._id = hit._id; - flattenedFields.node.ecs.timestamp = hit._source['@timestamp']; + flattenedFields.node.ecs.timestamp = (hit.fields['@timestamp'][0] ?? '') as string; flattenedFields.node.ecs._index = hit._index; if (hit.sort && hit.sort.length > 1) { flattenedFields.cursor.value = hit.sort[0]; @@ -40,13 +41,12 @@ const specialFields = ['_id', '_index', '_type', '_score']; const mergeTimelineFieldsWithHit = ( fieldName: string, flattenedFields: T, - hit: { _source: {} }, + hit: { fields: Record }, dataFields: readonly string[], ecsFields: readonly string[] ) => { if (fieldName != null || dataFields.includes(fieldName)) { - const esField = fieldName; - if (has(esField, hit._source) || specialFields.includes(esField)) { + if (has(fieldName, hit.fields) || specialFields.includes(fieldName)) { const objectWithProperty = { node: { ...get('node', flattenedFields), @@ -55,9 +55,11 @@ const mergeTimelineFieldsWithHit = ( ...get('node.data', flattenedFields), { field: fieldName, - value: specialFields.includes(esField) - ? toStringArray(get(esField, hit)) - : toStringArray(get(esField, hit._source)), + value: specialFields.includes(fieldName) + ? toStringArray(get(fieldName, hit)) + : isGeoField(fieldName) + ? formatGeoLocation(hit.fields[fieldName]) + : toStringArray(hit.fields[fieldName]), }, ] : get('node.data', flattenedFields), @@ -68,7 +70,7 @@ const mergeTimelineFieldsWithHit = ( ...fieldName.split('.').reduceRight( // @ts-expect-error (obj, next) => ({ [next]: obj }), - toStringArray(get(esField, hit._source)) + toStringArray(hit.fields[fieldName]) ), } : get('node.ecs', flattenedFields), diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts index 19535fa3dc8a8..de58e7cf44d64 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts @@ -9,11 +9,12 @@ import { cloneDeep, uniq } from 'lodash/fp'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { + EventHit, TimelineEventsQueries, TimelineEventsAllStrategyResponse, TimelineEventsAllRequestOptions, TimelineEdges, -} from '../../../../../../common/search_strategy/timeline'; +} from '../../../../../../common/search_strategy'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionTimelineFactory } from '../../types'; import { buildTimelineEventsAllQuery } from './query.events_all.dsl'; @@ -39,8 +40,7 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory - // @ts-expect-error - formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit) + formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit) ); const inspect = { dsl: [inspectStringifyObject(buildTimelineEventsAllQuery(queryOptions))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index a5a0c877ecdd3..034a2b3c6ea95 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -44,14 +44,11 @@ export const buildTimelineEventsAllQuery = ({ const filter = [...filterClause, ...getTimerangeFilter(timerange), { match_all: {} }]; - const getSortField = (sortField: SortField) => { - if (sortField.field) { - const field: string = sortField.field === 'timestamp' ? '@timestamp' : sortField.field; - - return [{ [field]: sortField.direction }]; - } - return []; - }; + const getSortField = (sortFields: SortField[]) => + sortFields.map((item) => { + const field: string = item.field === 'timestamp' ? '@timestamp' : item.field; + return { [field]: item.direction }; + }); const dslQuery = { allowNoIndices: true, @@ -68,7 +65,7 @@ export const buildTimelineEventsAllQuery = ({ size: querySize, track_total_hits: true, sort: getSortField(sort), - _source: fields, + fields, }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts new file mode 100644 index 0000000000000..34610da7d7aa3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts @@ -0,0 +1,153 @@ +/* + * 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 { EventHit } from '../../../../../../common/search_strategy'; +import { getDataFromHits } from './helpers'; + +describe('#getDataFromHits', () => { + it('happy path', () => { + const response: EventHit = { + _index: 'auditbeat-7.8.0-2020.11.05-000003', + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _score: 0, + _type: '', + fields: { + 'event.category': ['process'], + 'process.ppid': [3977], + 'user.name': ['jenkins'], + 'process.args': ['go', 'vet', './...'], + message: ['Process go (PID: 4313) by user jenkins STARTED'], + 'process.pid': [4313], + 'process.working_directory': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], + 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + 'process.name': ['go'], + 'event.action': ['process_started'], + 'agent.type': ['auditbeat'], + '@timestamp': ['2020-11-17T14:48:08.922Z'], + 'event.module': ['system'], + 'event.type': ['start'], + 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + 'host.os.family': ['debian'], + 'event.kind': ['event'], + 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], + 'event.dataset': ['process'], + 'process.executable': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + _source: {}, + sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], + aggregations: {}, + }; + + expect(getDataFromHits(response.fields)).toEqual([ + { + category: 'event', + field: 'event.category', + originalValue: ['process'], + values: ['process'], + }, + { category: 'process', field: 'process.ppid', originalValue: ['3977'], values: ['3977'] }, + { category: 'user', field: 'user.name', originalValue: ['jenkins'], values: ['jenkins'] }, + { + category: 'process', + field: 'process.args', + originalValue: ['go', 'vet', './...'], + values: ['go', 'vet', './...'], + }, + { + category: 'base', + field: 'message', + originalValue: ['Process go (PID: 4313) by user jenkins STARTED'], + values: ['Process go (PID: 4313) by user jenkins STARTED'], + }, + { category: 'process', field: 'process.pid', originalValue: ['4313'], values: ['4313'] }, + { + category: 'process', + field: 'process.working_directory', + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + { + category: 'process', + field: 'process.entity_id', + originalValue: ['Z59cIkAAIw8ZoK0H'], + values: ['Z59cIkAAIw8ZoK0H'], + }, + { + category: 'host', + field: 'host.ip', + originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + }, + { category: 'process', field: 'process.name', originalValue: ['go'], values: ['go'] }, + { + category: 'event', + field: 'event.action', + originalValue: ['process_started'], + values: ['process_started'], + }, + { + category: 'agent', + field: 'agent.type', + originalValue: ['auditbeat'], + values: ['auditbeat'], + }, + { + category: 'base', + field: '@timestamp', + originalValue: ['2020-11-17T14:48:08.922Z'], + values: ['2020-11-17T14:48:08.922Z'], + }, + { category: 'event', field: 'event.module', originalValue: ['system'], values: ['system'] }, + { category: 'event', field: 'event.type', originalValue: ['start'], values: ['start'] }, + { + category: 'host', + field: 'host.name', + originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + { + category: 'process', + field: 'process.hash.sha1', + originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + }, + { category: 'host', field: 'host.os.family', originalValue: ['debian'], values: ['debian'] }, + { category: 'event', field: 'event.kind', originalValue: ['event'], values: ['event'] }, + { + category: 'host', + field: 'host.id', + originalValue: ['e59991e835905c65ed3e455b33e13bd6'], + values: ['e59991e835905c65ed3e455b33e13bd6'], + }, + { + category: 'event', + field: 'event.dataset', + originalValue: ['process'], + values: ['process'], + }, + { + category: 'process', + field: 'process.executable', + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 2dd406ffaa450..68bef2e8c656a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; +import { isEmpty } from 'lodash/fp'; import { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy/timeline'; +import { toStringArray } from '../../../../helpers/to_array'; export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; @@ -18,39 +19,32 @@ export const getFieldCategory = (field: string): string => { return fieldCategory; }; -export const getDataFromHits = ( - sources: EventSource, - category?: string, - path?: string -): TimelineEventsDetailsItem[] => - Object.keys(sources).reduce((accumulator, source) => { - const item: EventSource = get(source, sources); - if (Array.isArray(item) || isString(item) || isNumber(item)) { - const field = path ? `${path}.${source}` : source; - const fieldCategory = getFieldCategory(field); +export const formatGeoLocation = (item: unknown[]) => { + const itemGeo = item.length > 0 ? (item[0] as { coordinates: number[] }) : null; + if (itemGeo != null && !isEmpty(itemGeo.coordinates)) { + try { + return toStringArray({ long: itemGeo.coordinates[0], lat: itemGeo.coordinates[1] }); + } catch { + return toStringArray(item); + } + } + return toStringArray(item); +}; - return [ - ...accumulator, - { - category: fieldCategory, - field, - values: Array.isArray(item) - ? item.map((value) => { - if (isObject(value)) { - return JSON.stringify(value); - } +export const isGeoField = (field: string) => + field.includes('geo.location') || field.includes('geoip.location'); - return value; - }) - : [item], - originalValue: item, - } as TimelineEventsDetailsItem, - ]; - } else if (isObject(item)) { - return [ - ...accumulator, - ...getDataFromHits(item, category || source, path ? `${path}.${source}` : source), - ]; - } - return accumulator; +export const getDataFromHits = (fields: Record): TimelineEventsDetailsItem[] => + Object.keys(fields).reduce((accumulator, field) => { + const item: unknown[] = fields[field]; + const fieldCategory = getFieldCategory(field); + return [ + ...accumulator, + { + category: fieldCategory, + field, + values: isGeoField(field) ? formatGeoLocation(item) : toStringArray(item), + originalValue: toStringArray(item), + } as TimelineEventsDetailsItem, + ]; }, []); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index 54e138c1e9d6f..0a011d2bfe878 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr, merge } from 'lodash/fp'; +import { cloneDeep, merge } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { @@ -27,13 +27,14 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory ): Promise => { const { indexName, eventId, docValueFields = [] } = options; - const sourceData = getOr({}, 'hits.hits.0._source', response.rawResponse); - const hitsData = getOr({}, 'hits.hits.0', response.rawResponse); + const fieldsData = cloneDeep(response.rawResponse.hits.hits[0].fields ?? {}); + const hitsData = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); delete hitsData._source; + delete hitsData.fields; const inspect = { dsl: [inspectStringifyObject(buildTimelineDetailsQuery(indexName, eventId, docValueFields))], }; - const data = getDataFromHits(merge(sourceData, hitsData)); + const data = getDataFromHits(merge(fieldsData, hitsData)); return { ...response, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index 216e8f947d261..8d70a08c214d8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -21,6 +21,7 @@ export const buildTimelineDetailsQuery = ( _id: [id], }, }, + fields: ['*'], }, size: 1, }); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx index 7d3ba92cf2ad7..c3d29bce57d54 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx @@ -23,7 +23,7 @@ import { } from '@elastic/eui'; import { Repository } from '../../../../../common/types'; -import { CronEditor, SectionError } from '../../../../shared_imports'; +import { Frequency, CronEditor, SectionError } from '../../../../shared_imports'; import { useServices } from '../../../app_context'; import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants'; import { useLoadRepositories } from '../../../services/http'; @@ -71,7 +71,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ // State for cron editor const [simpleCron, setSimpleCron] = useState<{ expression: string; - frequency: string; + frequency: Frequency; }>({ expression: DEFAULT_POLICY_SCHEDULE, frequency: DEFAULT_POLICY_FREQUENCY, @@ -480,6 +480,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ ) : ( = ({ cronExpression: expression, frequency, fieldToPreferredValueMap: newFieldToPreferredValueMap, - }: { - cronExpression: string; - frequency: string; - fieldToPreferredValueMap: any; }) => { setSimpleCron({ expression, diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx index 407b9be14e3c1..ee638edd09bb8 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx @@ -14,6 +14,7 @@ import { EuiButtonEmpty, EuiFieldNumber, EuiSelect, + EuiCode, } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../../common/types'; @@ -135,7 +136,10 @@ export const PolicyStepRetention: React.FunctionComponent = ({ description={ 200, + }} /> } fullWidth diff --git a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx index 2765006f9dcbc..40f37d6e67e90 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx @@ -25,7 +25,7 @@ import { import { useServices, useToastNotifications } from '../app_context'; import { documentationLinksService } from '../services/documentation'; -import { CronEditor } from '../../shared_imports'; +import { Frequency, CronEditor } from '../../shared_imports'; import { DEFAULT_RETENTION_SCHEDULE, DEFAULT_RETENTION_FREQUENCY } from '../constants'; import { updateRetentionSchedule } from '../services/http'; @@ -57,7 +57,7 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent({ expression: DEFAULT_RETENTION_SCHEDULE, frequency: DEFAULT_RETENTION_FREQUENCY, @@ -234,10 +234,6 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent { setSimpleCron({ expression, diff --git a/x-pack/plugins/snapshot_restore/public/application/constants/index.ts b/x-pack/plugins/snapshot_restore/public/application/constants/index.ts index 2f4945b625b53..1cf41da736e19 100644 --- a/x-pack/plugins/snapshot_restore/public/application/constants/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/constants/index.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DAY } from '../../shared_imports'; - export const BASE_PATH = ''; export const DEFAULT_SECTION: Section = 'snapshots'; export type Section = 'repositories' | 'snapshots' | 'restore_status' | 'policies'; @@ -89,10 +87,10 @@ export const REMOVE_INDEX_SETTINGS_SUGGESTIONS: string[] = INDEX_SETTING_SUGGEST ); export const DEFAULT_POLICY_SCHEDULE = '0 30 1 * * ?'; -export const DEFAULT_POLICY_FREQUENCY = DAY; +export const DEFAULT_POLICY_FREQUENCY = 'DAY'; export const DEFAULT_RETENTION_SCHEDULE = '0 30 1 * * ?'; -export const DEFAULT_RETENTION_FREQUENCY = DAY; +export const DEFAULT_RETENTION_FREQUENCY = 'DAY'; // UI Metric constants export const UIM_APP_NAME = 'snapshot_restore'; diff --git a/x-pack/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/plugins/snapshot_restore/public/shared_imports.ts index bd1c0e0cd395b..411ec8627c726 100644 --- a/x-pack/plugins/snapshot_restore/public/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/public/shared_imports.ts @@ -7,8 +7,8 @@ export { AuthorizationProvider, CronEditor, - DAY, Error, + Frequency, NotAuthorizedSection, SectionError, sendRequest, diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx index 54960fba731d2..bc26d6f132522 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx @@ -7,7 +7,7 @@ import { EuiButton, EuiCheckboxProps } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; -import { wait } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { mountWithIntl } from '@kbn/test/jest'; import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; @@ -70,7 +70,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(wrapper.find('input[name="name"]')).toHaveLength(1); }); @@ -132,7 +132,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(spacesManager.getSpace).toHaveBeenCalledWith('existing-space'); }); @@ -185,7 +185,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(notifications.toasts.addError).toHaveBeenCalledWith(error, { title: 'Error loading available features', @@ -223,7 +223,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(spacesManager.getSpace).toHaveBeenCalledWith('my-space'); }); @@ -285,7 +285,7 @@ describe('ManageSpacePage', () => { /> ); - await wait(() => { + await waitFor(() => { wrapper.update(); expect(spacesManager.getSpace).toHaveBeenCalledWith('my-space'); }); diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx index b08c1c834ac4f..e841d3efc828c 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.test.tsx @@ -13,7 +13,7 @@ import { SpacesManager } from '../spaces_manager'; import { NavControlPopover } from './nav_control_popover'; import { EuiHeaderSectionItemButton } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test/jest'; -import { wait } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; describe('NavControlPopover', () => { it('renders without crashing', () => { @@ -65,7 +65,7 @@ describe('NavControlPopover', () => { wrapper.find(EuiHeaderSectionItemButton).simulate('click'); // Wait for `getSpaces` promise to resolve - await wait(() => { + await waitFor(() => { wrapper.update(); expect(wrapper.find(SpaceAvatar)).toHaveLength(3); }); diff --git a/x-pack/plugins/stack_alerts/common/config.ts b/x-pack/plugins/stack_alerts/common/config.ts index 88d4699027425..a1ec8a1e1c454 100644 --- a/x-pack/plugins/stack_alerts/common/config.ts +++ b/x-pack/plugins/stack_alerts/common/config.ts @@ -8,7 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - enableGeoAlerts: schema.boolean({ defaultValue: false }), + enableGeoAlerting: schema.boolean({ defaultValue: false }), }); export type Config = TypeOf; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/index.ts index 9d611aefb738b..1a9710eb08eb0 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/index.ts @@ -17,7 +17,7 @@ export function registerAlertTypes({ alertTypeRegistry: TriggersAndActionsUIPublicPluginSetup['alertTypeRegistry']; config: Config; }) { - if (config.enableGeoAlerts) { + if (config.enableGeoAlerting) { alertTypeRegistry.register(getGeoThresholdAlertType()); alertTypeRegistry.register(getGeoContainmentAlertType()); } diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 4ac0bc43adcd7..448e1e698858b 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; import { GEO_THRESHOLD_ID as GeoThreshold } from './alert_types/geo_threshold/alert_type'; +import { GEO_CONTAINMENT_ID as GeoContainment } from './alert_types/geo_containment/alert_type'; import { STACK_ALERTS_FEATURE_ID } from '../common'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; @@ -20,7 +21,7 @@ export const BUILT_IN_ALERTS_FEATURE = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [IndexThreshold, GeoThreshold], + alerting: [IndexThreshold, GeoThreshold, GeoContainment], privileges: { all: { app: [], @@ -29,7 +30,7 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: [IndexThreshold, GeoThreshold], + all: [IndexThreshold, GeoThreshold, GeoContainment], read: [], }, savedObject: { @@ -47,7 +48,7 @@ export const BUILT_IN_ALERTS_FEATURE = { }, alerting: { all: [], - read: [IndexThreshold, GeoThreshold], + read: [IndexThreshold, GeoThreshold, GeoContainment], }, savedObject: { all: [], diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index 3ef8db33983de..08197b368d9d9 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -11,13 +11,13 @@ export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_ty export const config: PluginConfigDescriptor = { exposeToBrowser: { - enableGeoAlerts: true, + enableGeoAlerting: true, }, schema: configSchema, deprecations: ({ renameFromRoot }) => [ renameFromRoot( 'xpack.triggers_actions_ui.enableGeoTrackingThresholdAlert', - 'xpack.stack_alerts.enableGeoAlerts' + 'xpack.stack_alerts.enableGeoAlerting' ), ], }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 39b485d8875ba..7170d6699d82f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5218,11 +5218,8 @@ "xpack.apm.transactionDurationLabel": "期間", "xpack.apm.transactionErrorRateAlert.name": "トランザクションエラー率しきい値", "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "より大きい", - "xpack.apm.transactions.chart.95thPercentileLabel": "95 パーセンタイル", - "xpack.apm.transactions.chart.99thPercentileLabel": "99 パーセンタイル", "xpack.apm.transactions.chart.anomalyBoundariesLabel": "異常境界", "xpack.apm.transactions.chart.anomalyScoreLabel": "異常スコア", - "xpack.apm.transactions.chart.averageLabel": "平均", "xpack.apm.transactionsTable.95thPercentileColumnLabel": "95 パーセンタイル", "xpack.apm.transactionsTable.avgDurationColumnLabel": "平均期間", "xpack.apm.transactionsTable.impactColumnDescription": "ご利用のサービスで最も頻繁に使用されていて、最も遅いエンドポイントです。相対的平均時間に 1 分ごとのトランザクション数をかけて計算されます。", @@ -7190,22 +7187,15 @@ "xpack.fleet.agentList.policyFilterText": "エージェントポリシー", "xpack.fleet.agentList.reassignActionText": "新しいポリシーに割り当てる", "xpack.fleet.agentList.revisionNumber": "rev. {revNumber}", - "xpack.fleet.agentList.showInactiveSwitchLabel": "非アクティブ", "xpack.fleet.agentList.showUpgradeableFilterLabel": "アップグレードが利用可能です", "xpack.fleet.agentList.statusColumnTitle": "ステータス", - "xpack.fleet.agentList.statusErrorFilterText": "エラー", "xpack.fleet.agentList.statusFilterText": "ステータス", "xpack.fleet.agentList.statusOfflineFilterText": "オフライン", - "xpack.fleet.agentList.statusOnlineFilterText": "オンライン", "xpack.fleet.agentList.statusUpdatingFilterText": "更新中", "xpack.fleet.agentList.unenrollOneButton": "エージェントの登録解除", "xpack.fleet.agentList.upgradeOneButton": "エージェントをアップグレード", "xpack.fleet.agentList.versionTitle": "バージョン", "xpack.fleet.agentList.viewActionText": "エージェントを表示", - "xpack.fleet.agentListStatus.errorLabel": "エラー", - "xpack.fleet.agentListStatus.offlineLabel": "オフライン", - "xpack.fleet.agentListStatus.onlineLabel": "オンライン", - "xpack.fleet.agentListStatus.totalLabel": "エージェント", "xpack.fleet.agentPolicy.confirmModalCalloutDescription": "選択されたエージェントポリシー{policyName}が一部のエージェントですでに使用されていることをFleetが検出しました。このアクションの結果として、Fleetはこのポリシーで使用されているすべてのエージェントに更新をデプロイします。", "xpack.fleet.agentPolicy.confirmModalCancelButtonLabel": "キャンセル", "xpack.fleet.agentPolicy.confirmModalConfirmButtonLabel": "変更を保存してデプロイ", @@ -7359,7 +7349,6 @@ "xpack.fleet.dataStreamList.viewDashboardActionText": "ダッシュボードを表示", "xpack.fleet.dataStreamList.viewDashboardsActionText": "ダッシュボードを表示", "xpack.fleet.dataStreamList.viewDashboardsPanelTitle": "ダッシュボードを表示", - "xpack.fleet.defaultSearchPlaceholderText": "検索", "xpack.fleet.deleteAgentPolicy.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {#個のエージェントは} other {#個のエージェントは}}このエージェントポリシーに割り当てられました。このポリシーを削除する前に、これらのエージェントの割り当てを解除します。", "xpack.fleet.deleteAgentPolicy.confirmModal.affectedAgentsTitle": "使用中のポリシー", "xpack.fleet.deleteAgentPolicy.confirmModal.cancelButtonLabel": "キャンセル", @@ -7758,7 +7747,6 @@ "xpack.graph.listing.table.entityName": "グラフ", "xpack.graph.listing.table.entityNamePlural": "グラフ", "xpack.graph.listing.table.titleColumnName": "タイトル", - "xpack.graph.loadWorkspace.missingIndexPatternErrorMessage": "インデックスパターンが見つかりませんでした", "xpack.graph.missingWorkspaceErrorMessage": "ID でグラフを読み込めませんでした", "xpack.graph.newGraphTitle": "保存されていないグラフ", "xpack.graph.noDataSourceNotificationMessageText": "データソースが見つかりませんでした。{managementIndexPatternsLink} に移動して Elasticsearch インデックスのインデックスパターンを作成してください。", @@ -9044,8 +9032,6 @@ "xpack.indexLifecycleMgmt.editPolicy.warmPhase.activateWarmPhaseSwitchLabel": "ウォームフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.indexPriorityExplanationText": "ノードの再起動後にインデックスを復元する優先順位を設定します。優先順位の高いインデックスは優先順位の低いインデックスよりも先に復元されます。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel": "レプリカを設定", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.shrinkIndexExplanationText": "インデックス情報をプライマリシャードの少ない新規インデックスに縮小します。", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.shrinkText": "縮小", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescriptionMessage": "まだインデックスにクエリを実行中ですが、読み取り専用です。性能の低いハードウェアにシャードを割り当てることができます。検索を高速化するために、シャードの数を減らしセグメントを結合することができます。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseLabel": "ウォームフェーズ", "xpack.indexLifecycleMgmt.featureCatalogueDescription": "ライフサイクルポリシーを定義し、インデックス年齢として自動的に処理を実行します。", @@ -9176,11 +9162,9 @@ "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "ウォームティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。", "xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel": "ロールオーバー時にウォームフェーズに変更", - "xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "プライマリシャードの数", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription": "レプリカの数を設定します。デフォルトでは前のフェーズと同じです。", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "レプリカの数", "xpack.indexLifecycleMgmt.warmPhase.replicasTitle": "レプリカ", - "xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "インデックスを縮小", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし(グループなし)", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", "xpack.infra.alerting.alertsButton": "アラート", @@ -11302,7 +11286,6 @@ "xpack.maps.source.emsTile.settingsTitle": "ベースマップ", "xpack.maps.source.emsTileDescription": "Elastic Maps Service のマップタイル", "xpack.maps.source.emsTileTitle": "タイル", - "xpack.maps.source.esAggSource.topTermLabel": "トップ {fieldName}", "xpack.maps.source.esGeoGrid.geofieldLabel": "地理空間フィールド", "xpack.maps.source.esGeoGrid.geofieldPlaceholder": "ジオフィールドを選択", "xpack.maps.source.esGeoGrid.gridRectangleDropdownOption": "グリッド", @@ -17915,7 +17898,6 @@ "xpack.securitySolution.resolver.eventDescription.networkEventLabel": "{ networkDirection } { forwardedIP }", "xpack.securitySolution.resolver.eventDescription.registryKeyLabel": "{ registryKey }", "xpack.securitySolution.resolver.eventDescription.registryPathLabel": "{ registryPath }", - "xpack.securitySolution.resolver.node_icon": "{running, select, true {実行中のプロセス} false {終了したプロセス}}", "xpack.securitySolution.resolver.panel.copyToClipboard": "クリップボードにコピー", "xpack.securitySolution.resolver.panel.eventDetail.requestError": "イベント詳細を取得できませんでした", "xpack.securitySolution.resolver.panel.nodeList.title": "すべてのプロセスイベント", @@ -18134,13 +18116,9 @@ "xpack.securitySolution.trustedapps.list.columns.actions": "アクション", "xpack.securitySolution.trustedapps.list.pageTitle": "信頼できるアプリケーション", "xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, one {#個の信頼できるアプリケーション} other {#個の信頼できるアプリケーション}}", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field": "フィールド", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash": "ハッシュ", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path": "パス", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator": "演算子", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator.is": "is", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "エントリを削除", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.value": "値", "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", "xpack.securitySolution.trustedapps.noResults": "項目が見つかりません", @@ -18377,7 +18355,6 @@ "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescription": "スナップショットの名前です。それぞれの名前に自動的に追加される固有の識別子です。", "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescriptionTitle": "スナップショット名", "xpack.snapshotRestore.policyForm.stepLogisticsTitle": "ロジスティクス", - "xpack.snapshotRestore.policyForm.stepRetention.countDescription": "クラスターに格納するスナップショットの最少数と最大数。", "xpack.snapshotRestore.policyForm.stepRetention.countTitle": "保存するスナップショット", "xpack.snapshotRestore.policyForm.stepRetention.docsButtonLabel": "スナップショット保存ドキュメント", "xpack.snapshotRestore.policyForm.stepRetention.expirationDescription": "スナップショットの削除までに待つ時間です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c3b910c47b121..d2e9a63141272 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5222,11 +5222,8 @@ "xpack.apm.transactionDurationLabel": "持续时间", "xpack.apm.transactionErrorRateAlert.name": "事务错误率阈值", "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "高于", - "xpack.apm.transactions.chart.95thPercentileLabel": "第 95 个百分位", - "xpack.apm.transactions.chart.99thPercentileLabel": "第 99 个百分位", "xpack.apm.transactions.chart.anomalyBoundariesLabel": "异常边界", "xpack.apm.transactions.chart.anomalyScoreLabel": "异常分数", - "xpack.apm.transactions.chart.averageLabel": "平均", "xpack.apm.transactionsTable.95thPercentileColumnLabel": "第 95 个百分位", "xpack.apm.transactionsTable.avgDurationColumnLabel": "平均持续时间", "xpack.apm.transactionsTable.impactColumnDescription": "服务中最常用的和最慢的终端节点。其计算方法是相对平均持续时间乘以每分钟事务数。", @@ -7196,22 +7193,15 @@ "xpack.fleet.agentList.policyFilterText": "代理策略", "xpack.fleet.agentList.reassignActionText": "分配到新策略", "xpack.fleet.agentList.revisionNumber": "修订 {revNumber}", - "xpack.fleet.agentList.showInactiveSwitchLabel": "非活动", "xpack.fleet.agentList.showUpgradeableFilterLabel": "升级可用", "xpack.fleet.agentList.statusColumnTitle": "状态", - "xpack.fleet.agentList.statusErrorFilterText": "错误", "xpack.fleet.agentList.statusFilterText": "状态", "xpack.fleet.agentList.statusOfflineFilterText": "脱机", - "xpack.fleet.agentList.statusOnlineFilterText": "联机", "xpack.fleet.agentList.statusUpdatingFilterText": "正在更新", "xpack.fleet.agentList.unenrollOneButton": "取消注册代理", "xpack.fleet.agentList.upgradeOneButton": "升级代理", "xpack.fleet.agentList.versionTitle": "版本", "xpack.fleet.agentList.viewActionText": "查看代理", - "xpack.fleet.agentListStatus.errorLabel": "错误", - "xpack.fleet.agentListStatus.offlineLabel": "脱机", - "xpack.fleet.agentListStatus.onlineLabel": "联机", - "xpack.fleet.agentListStatus.totalLabel": "代理", "xpack.fleet.agentPolicy.confirmModalCalloutDescription": "Fleet 检测到您的部分代理已在使用选定代理策略 {policyName}。由于此操作,Fleet 会将更新部署到使用此策略的所有代理。", "xpack.fleet.agentPolicy.confirmModalCalloutTitle": "此操作将更新 {agentCount, plural, one {# 个代理} other {# 个代理}}", "xpack.fleet.agentPolicy.confirmModalCancelButtonLabel": "取消", @@ -7366,7 +7356,6 @@ "xpack.fleet.dataStreamList.viewDashboardActionText": "查看仪表板", "xpack.fleet.dataStreamList.viewDashboardsActionText": "查看仪表板", "xpack.fleet.dataStreamList.viewDashboardsPanelTitle": "查看仪表板", - "xpack.fleet.defaultSearchPlaceholderText": "搜索", "xpack.fleet.deleteAgentPolicy.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# 个代理} other {# 个代理}}已分配到此代理策略。在删除此策略前取消分配这些代理。", "xpack.fleet.deleteAgentPolicy.confirmModal.affectedAgentsTitle": "在用的策略", "xpack.fleet.deleteAgentPolicy.confirmModal.cancelButtonLabel": "取消", @@ -7766,7 +7755,6 @@ "xpack.graph.listing.table.entityName": "图表", "xpack.graph.listing.table.entityNamePlural": "图表", "xpack.graph.listing.table.titleColumnName": "标题", - "xpack.graph.loadWorkspace.missingIndexPatternErrorMessage": "未找到索引模式", "xpack.graph.missingWorkspaceErrorMessage": "无法使用 ID 加载图表", "xpack.graph.newGraphTitle": "未保存图表", "xpack.graph.noDataSourceNotificationMessageText": "未找到数据源。前往 {managementIndexPatternsLink},为您的 Elasticsearch 索引创建索引模式。", @@ -9053,8 +9041,6 @@ "xpack.indexLifecycleMgmt.editPolicy.warmPhase.activateWarmPhaseSwitchLabel": "激活温阶段", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.indexPriorityExplanationText": "设置在节点重新启动后恢复索引的优先级。较高优先级的索引会在较低优先级的索引之前恢复。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel": "设置副本", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.shrinkIndexExplanationText": "将索引缩小成具有较少主分片的新索引。", - "xpack.indexLifecycleMgmt.editPolicy.warmPhase.shrinkText": "缩小", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescriptionMessage": "您仍在查询自己的索引,但其为只读。您可以将分片分配给效率较低的硬件。为了获取更快的搜索,您可以减少分片数目并强制合并段。", "xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseLabel": "温阶段", "xpack.indexLifecycleMgmt.featureCatalogueDescription": "定义生命周期策略,以随着索引老化自动执行操作。", @@ -9185,11 +9171,9 @@ "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "没有分配到温层的节点", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。", "xpack.indexLifecycleMgmt.warmPhase.moveToWarmPhaseOnRolloverLabel": "滚动更新时移到温阶段", - "xpack.indexLifecycleMgmt.warmPhase.numberOfPrimaryShardsLabel": "主分片数目", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasDescription": "设置副本数目。默认情况下与上一阶段相同。", "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "副本分片数目", "xpack.indexLifecycleMgmt.warmPhase.replicasTitle": "副本", - "xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel": "缩小索引", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容(未分组)", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", "xpack.infra.alerting.alertsButton": "告警", @@ -11315,7 +11299,6 @@ "xpack.maps.source.emsTile.settingsTitle": "Basemap", "xpack.maps.source.emsTileDescription": "Elastic 地图服务的地图磁贴", "xpack.maps.source.emsTileTitle": "磁贴", - "xpack.maps.source.esAggSource.topTermLabel": "热门{fieldName}", "xpack.maps.source.esGeoGrid.geofieldLabel": "地理空间字段", "xpack.maps.source.esGeoGrid.geofieldPlaceholder": "选择地理字段", "xpack.maps.source.esGeoGrid.gridRectangleDropdownOption": "网格", @@ -17933,7 +17916,6 @@ "xpack.securitySolution.resolver.eventDescription.networkEventLabel": "{ networkDirection } { forwardedIP }", "xpack.securitySolution.resolver.eventDescription.registryKeyLabel": "{ registryKey }", "xpack.securitySolution.resolver.eventDescription.registryPathLabel": "{ registryPath }", - "xpack.securitySolution.resolver.node_icon": "{running, select, true {正在运行的进程} false {已终止的进程}}", "xpack.securitySolution.resolver.panel.copyToClipboard": "复制到剪贴板", "xpack.securitySolution.resolver.panel.eventDetail.requestError": "无法检索事件详情", "xpack.securitySolution.resolver.panel.nodeList.title": "所有进程事件", @@ -18152,13 +18134,9 @@ "xpack.securitySolution.trustedapps.list.columns.actions": "操作", "xpack.securitySolution.trustedapps.list.pageTitle": "受信任的应用程序", "xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, one {# 个受信任的应用程序} other {# 个受信任的应用程序}}", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field": "字段", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash": "哈希", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path": "路径", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator": "运算符", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.operator.is": "is", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "移除条目", - "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.value": "值", "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", "xpack.securitySolution.trustedapps.noResults": "找不到项目", @@ -18395,7 +18373,6 @@ "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescription": "快照的名称。唯一标识符将自动添加到每个名称中。", "xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescriptionTitle": "快照名称", "xpack.snapshotRestore.policyForm.stepLogisticsTitle": "运筹", - "xpack.snapshotRestore.policyForm.stepRetention.countDescription": "在您的集群中要存储的最小和最大快照数目。", "xpack.snapshotRestore.policyForm.stepRetention.countTitle": "要保留的快照", "xpack.snapshotRestore.policyForm.stepRetention.docsButtonLabel": "快照保留文档", "xpack.snapshotRestore.policyForm.stepRetention.expirationDescription": "删除快照前要等候的时间。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 5b2c8bd63a2f5..9de3ae21a8ef7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -11,6 +11,11 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, Alert, AlertAction } from '../../../types'; import ActionForm from './action_form'; import { useKibana } from '../../../common/lib/kibana'; +import { + RecoveredActionGroup, + isActionGroupDisabledForActionTypeId, +} from '../../../../../alerts/common'; + jest.mock('../../../common/lib/kibana'); jest.mock('../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), @@ -65,6 +70,21 @@ describe('action_form', () => { actionParamsFields: mockedActionParamsFields, }; + const disabledByActionType = { + id: '.jira', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: mockedActionParamsFields, + }; + const disabledByLicenseActionType = { id: 'disabled-by-license', iconClass: 'test', @@ -112,7 +132,7 @@ describe('action_form', () => { const useKibanaMock = useKibana as jest.Mocked; describe('action_form in alert', () => { - async function setup(customActions?: AlertAction[]) { + async function setup(customActions?: AlertAction[], customRecoveredActionGroup?: string) { const actionTypeRegistry = actionTypeRegistryMock.create(); const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); @@ -159,6 +179,14 @@ describe('action_form', () => { }, isPreconfigured: false, }, + { + secrets: {}, + id: '.jira', + actionTypeId: disabledByActionType.id, + name: 'Connector with disabled action group', + config: {}, + isPreconfigured: false, + }, ]); const mocks = coreMock.createSetup(); const [ @@ -179,6 +207,7 @@ describe('action_form', () => { actionType, disabledByConfigActionType, disabledByLicenseActionType, + disabledByActionType, preconfiguredOnly, actionTypeWithoutParams, ]); @@ -223,12 +252,24 @@ describe('action_form', () => { context: [{ name: 'contextVar', description: 'context var1' }], }} defaultActionGroupId={'default'} + isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) => { + const recoveryActionGroupId = customRecoveredActionGroup + ? customRecoveredActionGroup + : 'recovered'; + return isActionGroupDisabledForActionTypeId( + actionGroupId === recoveryActionGroupId ? RecoveredActionGroup.id : actionGroupId, + actionTypeId + ); + }} setActionIdByIndex={(id: string, index: number) => { initialAlert.actions[index].id = id; }} actionGroups={[ { id: 'default', name: 'Default', defaultActionMessage }, - { id: 'recovered', name: 'Recovered' }, + { + id: customRecoveredActionGroup ? customRecoveredActionGroup : 'recovered', + name: customRecoveredActionGroup ? 'I feel better' : 'Recovered', + }, ]} setActionGroupIdByIndex={(group: string, index: number) => { initialAlert.actions[index].group = group; @@ -280,6 +321,14 @@ describe('action_form', () => { enabledInLicense: false, minimumLicenseRequired: 'gold', }, + { + id: '.jira', + name: 'Disabled by action type', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, { id: actionTypeWithoutParams.id, name: 'Action type without params', @@ -342,11 +391,13 @@ describe('action_form', () => { Array [ Object { "data-test-subj": "addNewActionConnectorActionGroup-0-option-default", + "disabled": false, "inputDisplay": "Default", "value": "default", }, Object { "data-test-subj": "addNewActionConnectorActionGroup-0-option-recovered", + "disabled": false, "inputDisplay": "Recovered", "value": "recovered", }, @@ -354,6 +405,77 @@ describe('action_form', () => { `); }); + it('renders disabled action groups for selected action type', async () => { + const wrapper = await setup([ + { + group: 'recovered', + id: 'test', + actionTypeId: disabledByActionType.id, + params: { + message: '', + }, + }, + ]); + const actionOption = wrapper.find(`[data-test-subj=".jira-ActionTypeSelectOption"]`); + actionOption.first().simulate('click'); + const actionGroupsSelect = wrapper.find( + `[data-test-subj="addNewActionConnectorActionGroup-1"]` + ); + expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "addNewActionConnectorActionGroup-1-option-default", + "disabled": false, + "inputDisplay": "Default", + "value": "default", + }, + Object { + "data-test-subj": "addNewActionConnectorActionGroup-1-option-recovered", + "disabled": true, + "inputDisplay": "Recovered (Not Currently Supported)", + "value": "recovered", + }, + ] + `); + }); + + it('renders disabled action groups for custom recovered action groups', async () => { + const wrapper = await setup( + [ + { + group: 'iHaveRecovered', + id: 'test', + actionTypeId: disabledByActionType.id, + params: { + message: '', + }, + }, + ], + 'iHaveRecovered' + ); + const actionOption = wrapper.find(`[data-test-subj=".jira-ActionTypeSelectOption"]`); + actionOption.first().simulate('click'); + const actionGroupsSelect = wrapper.find( + `[data-test-subj="addNewActionConnectorActionGroup-1"]` + ); + expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "addNewActionConnectorActionGroup-1-option-default", + "disabled": false, + "inputDisplay": "Default", + "value": "default", + }, + Object { + "data-test-subj": "addNewActionConnectorActionGroup-1-option-iHaveRecovered", + "disabled": true, + "inputDisplay": "I feel better (Not Currently Supported)", + "value": "iHaveRecovered", + }, + ] + `); + }); + it('renders available connectors for the selected action type', async () => { const wrapper = await setup(); const actionOption = wrapper.find( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 0337f6879e24a..1cb1a68986192 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -59,6 +59,7 @@ export interface ActionAccordionFormProps { setHasActionsWithBrokenConnector?: (value: boolean) => void; actionTypeRegistry: ActionTypeRegistryContract; getDefaultActionParams?: DefaultActionParamsGetter; + isActionGroupDisabledForActionType?: (actionGroupId: string, actionTypeId: string) => boolean; } interface ActiveActionConnectorState { @@ -81,6 +82,7 @@ export const ActionForm = ({ setHasActionsWithBrokenConnector, actionTypeRegistry, getDefaultActionParams, + isActionGroupDisabledForActionType, }: ActionAccordionFormProps) => { const { http, @@ -345,6 +347,7 @@ export const ActionForm = ({ actionGroups={actionGroups} defaultActionMessage={defaultActionMessage} defaultParams={getDefaultActionParams?.(actionItem.actionTypeId, actionItem.group)} + isActionGroupDisabledForActionType={isActionGroupDisabledForActionType} setActionGroupIdByIndex={setActionGroupIdByIndex} onAddConnector={() => { setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index d68f66f373135..9a721b2f2bed0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -60,6 +60,7 @@ export type ActionTypeFormProps = { connectors: ActionConnector[]; actionTypeRegistry: ActionTypeRegistryContract; defaultParams: DefaultActionParams; + isActionGroupDisabledForActionType?: (actionGroupId: string, actionTypeId: string) => boolean; } & Pick< ActionAccordionFormProps, | 'defaultActionGroupId' @@ -94,6 +95,7 @@ export const ActionTypeForm = ({ actionGroups, setActionGroupIdByIndex, actionTypeRegistry, + isActionGroupDisabledForActionType, defaultParams, }: ActionTypeFormProps) => { const { @@ -145,6 +147,28 @@ export const ActionTypeForm = ({ const actionType = actionTypesIndex[actionItem.actionTypeId]; + const actionGroupDisplay = ( + actionGroupId: string, + actionGroupName: string, + actionTypeId: string + ): string => + isActionGroupDisabledForActionType + ? isActionGroupDisabledForActionType(actionGroupId, actionTypeId) + ? i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.addNewActionConnectorActionGroup.display', + { + defaultMessage: '{actionGroupName} (Not Currently Supported)', + values: { actionGroupName }, + } + ) + : actionGroupName + : actionGroupName; + + const isActionGroupDisabled = (actionGroupId: string, actionTypeId: string): boolean => + isActionGroupDisabledForActionType + ? isActionGroupDisabledForActionType(actionGroupId, actionTypeId) + : false; + const optionsList = connectors .filter( (connectorItem) => @@ -191,7 +215,8 @@ export const ActionTypeForm = ({ data-test-subj={`addNewActionConnectorActionGroup-${index}`} options={actionGroups.map(({ id: value, name }) => ({ value, - inputDisplay: name, + inputDisplay: actionGroupDisplay(value, name, actionItem.actionTypeId), + disabled: isActionGroupDisabled(value, actionItem.actionTypeId), 'data-test-subj': `addNewActionConnectorActionGroup-${index}-option-${value}`, }))} valueOfSelected={selectedActionGroup.id} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index 7cd95c92b22a3..3264f22bb928f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -12,6 +12,7 @@ import { loadActionTypes } from '../../lib/action_connector_api'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { useKibana } from '../../../common/lib/kibana'; +import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../..'; interface Props { onActionTypeChange: (actionType: ActionType) => void; @@ -35,7 +36,18 @@ export const ActionTypeMenu = ({ useEffect(() => { (async () => { try { - const availableActionTypes = actionTypes ?? (await loadActionTypes({ http })); + /** + * Hidden action types will be hidden only on Alerts & Actions. + * actionTypes prop is not filtered. Thus, any consumer that provides it's own actionTypes + * can use the hidden action types. For example, Cases or Detections of Security Solution. + * + * TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. + * */ + const availableActionTypes = + actionTypes ?? + (await loadActionTypes({ http })).filter( + (actionType) => !DEFAULT_HIDDEN_ACTION_TYPES.includes(actionType.id) + ); const index: ActionTypeIndex = {}; for (const actionTypeItem of availableActionTypes) { index[actionTypeItem.id] = actionTypeItem; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index fed888b40ad86..2df75436f5f96 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -39,6 +39,7 @@ import './actions_connectors_list.scss'; import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types'; import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt'; import { useKibana } from '../../../../common/lib/kibana'; +import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../'; export const ActionsConnectorsList: React.FunctionComponent = () => { const { @@ -94,18 +95,23 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { }, []); const actionConnectorTableItems: ActionConnectorTableItem[] = actionTypesIndex - ? actions.map((action) => { - return { - ...action, - actionType: actionTypesIndex[action.actionTypeId] - ? actionTypesIndex[action.actionTypeId].name - : action.actionTypeId, - }; - }) + ? actions + // TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. + .filter((action) => !DEFAULT_HIDDEN_ACTION_TYPES.includes(action.actionTypeId)) + .map((action) => { + return { + ...action, + actionType: actionTypesIndex[action.actionTypeId] + ? actionTypesIndex[action.actionTypeId].name + : action.actionTypeId, + }; + }) : []; const actionTypesList: Array<{ value: string; name: string }> = actionTypesIndex ? Object.values(actionTypesIndex) + // TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. + .filter((actionType) => !DEFAULT_HIDDEN_ACTION_TYPES.includes(actionType.id)) .map((actionType) => ({ value: actionType.id, name: `${actionType.name} (${getActionsCountByActionType(actions, actionType.id)})`, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index c94086a6adab9..3a8835825acd1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -55,7 +55,12 @@ import { } from '../../../types'; import { getTimeOptions } from '../../../common/lib/get_time_options'; import { ActionForm } from '../action_connector_form'; -import { AlertActionParam, ALERTS_FEATURE_ID } from '../../../../../alerts/common'; +import { + AlertActionParam, + ALERTS_FEATURE_ID, + RecoveredActionGroup, + isActionGroupDisabledForActionTypeId, +} from '../../../../../alerts/common'; import { hasAllPrivilege, hasShowActionsCapability } from '../../lib/capabilities'; import { SolutionFilter } from './solution_filter'; import './alert_form.scss'; @@ -192,6 +197,7 @@ export const AlertForm = ({ setDefaultActionGroupId(index.get(alert.alertTypeId)!.defaultActionGroupId); } setAlertTypesIndex(index); + const availableAlertTypesResult = getAvailableAlertTypes(alertTypesResult); setAvailableAlertTypes(availableAlertTypesResult); @@ -331,6 +337,18 @@ export const AlertForm = ({ const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : []; + const isActionGroupDisabledForActionType = useCallback( + (alertType: AlertType, actionGroupId: string, actionTypeId: string): boolean => { + return isActionGroupDisabledForActionTypeId( + actionGroupId === alertType?.recoveryActionGroup?.id + ? RecoveredActionGroup.id + : actionGroupId, + actionTypeId + ); + }, + [] + ); + const AlertParamsExpressionComponent = alertTypeModel ? alertTypeModel.alertParamsExpression : null; @@ -513,6 +531,9 @@ export const AlertForm = ({ setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector} messageVariables={selectedAlertType.actionVariables} defaultActionGroupId={defaultActionGroupId} + isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) => + isActionGroupDisabledForActionType(selectedAlertType, actionGroupId, actionTypeId) + } actionGroups={selectedAlertType.actionGroups.map((actionGroup) => actionGroup.id === selectedAlertType.recoveryActionGroup.id ? { diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index dab28fb03f4e0..780cb05d31d8d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { fireEvent, render, wait, cleanup } from '@testing-library/react'; +import { fireEvent, render, waitFor, cleanup } from '@testing-library/react'; import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns'; import { mockGetTriggerInfo, @@ -50,7 +50,7 @@ test('Allows to manage drilldowns', async () => { ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); // no drilldowns in the list expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0); @@ -87,7 +87,7 @@ test('Allows to manage drilldowns', async () => { expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible(); - await wait(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(1)); + await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(1)); expect(screen.getByText(name)).toBeVisible(); const editButton = screen.getByText(/edit/i); fireEvent.click(editButton); @@ -105,14 +105,14 @@ test('Allows to manage drilldowns', async () => { fireEvent.click(screen.getByText(/save/i)); expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible(); - await wait(() => screen.getByText(newName)); + await waitFor(() => screen.getByText(newName)); // delete drilldown from edit view fireEvent.click(screen.getByText(/edit/i)); fireEvent.click(screen.getByText(/delete/i)); expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible(); - await wait(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); + await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); }); test('Can delete multiple drilldowns', async () => { @@ -123,7 +123,7 @@ test('Can delete multiple drilldowns', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); const createDrilldown = async () => { const oldCount = screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM).length; @@ -136,7 +136,7 @@ test('Can delete multiple drilldowns', async () => { target: { value: 'https://elastic.co' }, }); fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); - await wait(() => + await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(oldCount + 1) ); }; @@ -151,7 +151,7 @@ test('Can delete multiple drilldowns', async () => { expect(screen.queryByText(/Create/i)).not.toBeInTheDocument(); fireEvent.click(screen.getByText(/Delete \(3\)/i)); - await wait(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); + await waitFor(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); }); test('Create only mode', async () => { @@ -165,7 +165,7 @@ test('Create only mode', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'test' }, }); @@ -175,7 +175,7 @@ test('Create only mode', async () => { }); fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); - await wait(() => expect(toasts.addSuccess).toBeCalled()); + await waitFor(() => expect(toasts.addSuccess).toBeCalled()); expect(onClose).toBeCalled(); expect(await mockDynamicActionManager.state.get().events.length).toBe(1); }); @@ -189,7 +189,7 @@ test('After switching between action factories state is restored', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'test' }, }); @@ -210,7 +210,7 @@ test('After switching between action factories state is restored', async () => { expect(screen.getByLabelText(/name/i)).toHaveValue('test'); fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); - await wait(() => expect(toasts.addSuccess).toBeCalled()); + await waitFor(() => expect(toasts.addSuccess).toBeCalled()); expect(await (mockDynamicActionManager.state.get().events[0].action.config as any).url).toBe( 'https://elastic.co' ); @@ -230,7 +230,7 @@ test("Error when can't save drilldown changes", async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); fireEvent.click(screen.getByText(/Create new/i)); fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'test' }, @@ -240,7 +240,7 @@ test("Error when can't save drilldown changes", async () => { target: { value: 'https://elastic.co' }, }); fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); - await wait(() => + await waitFor(() => expect(toasts.addError).toBeCalledWith(error, { title: toastDrilldownsCRUDError }) ); }); @@ -254,7 +254,7 @@ test('Should show drilldown welcome message. Should be able to dismiss it', asyn ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); expect(screen.getByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeVisible(); fireEvent.click(screen.getByText(/hide/i)); @@ -268,7 +268,7 @@ test('Should show drilldown welcome message. Should be able to dismiss it', asyn /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + await waitFor(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull(); }); @@ -281,7 +281,7 @@ test('Drilldown type is not shown if no supported trigger', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); expect(screen.queryByText(/Go to Dashboard/i)).not.toBeInTheDocument(); // dashboard action is not visible, because APPLY_FILTER_TRIGGER not supported expect(screen.getByTestId('selectedActionFactory-Url')).toBeInTheDocument(); }); @@ -295,7 +295,7 @@ test('Can pick a trigger', async () => { /> ); // wait for initial render. It is async because resolving compatible action factories is async - await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + await waitFor(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); // input drilldown name const name = 'Test name'; @@ -318,6 +318,6 @@ test('Can pick a trigger', async () => { expect(createButton).toBeEnabled(); fireEvent.click(createButton); - await wait(() => expect(toasts.addSuccess).toBeCalled()); + await waitFor(() => expect(toasts.addSuccess).toBeCalled()); expect(mockDynamicActionManager.state.get().events[0].triggers).toEqual(['SELECT_RANGE_TRIGGER']); }); diff --git a/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx b/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx index a4c6e24f5de5e..c99b6aeb8ef8f 100644 --- a/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx @@ -14,6 +14,7 @@ import { CertMonitors } from './cert_monitors'; import * as labels from './translations'; import { Cert, CertMonitor } from '../../../common/runtime_types'; import { FingerprintCol } from './fingerprint_col'; +import { NO_CERTS_AVAILABLE } from './translations'; interface Page { index: number; @@ -109,6 +110,7 @@ export const CertificateList: React.FC = ({ page, sort, onChange }) => { direction: sort.direction, }, }} + noItemsMessage={{NO_CERTS_AVAILABLE}} /> ); }; diff --git a/x-pack/plugins/uptime/public/components/certificates/translations.ts b/x-pack/plugins/uptime/public/components/certificates/translations.ts index 176625d647ca0..5abda745d9c86 100644 --- a/x-pack/plugins/uptime/public/components/certificates/translations.ts +++ b/x-pack/plugins/uptime/public/components/certificates/translations.ts @@ -65,3 +65,7 @@ export const FINGERPRINTS_COL = i18n.translate('xpack.uptime.certs.list.expirati export const COPY_FINGERPRINT = i18n.translate('xpack.uptime.certs.list.copyFingerprint', { defaultMessage: 'Click to copy fingerprint value', }); + +export const NO_CERTS_AVAILABLE = i18n.translate('xpack.uptime.certs.list.empty', { + defaultMessage: 'No Certificates found. Note: Certificates are only visible for Heartbeat 7.8+', +}); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index d832902fe066d..13e5c66b73460 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -12,26 +12,23 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../.. import { defineAlertTypes } from './alert_types'; import { defineActionTypes } from './action_types'; import { defineRoutes } from './routes'; -import { SpacesPluginSetup } from '../../../../../../../plugins/spaces/server'; -import { SecurityPluginSetup } from '../../../../../../../plugins/security/server'; +import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; +import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; actions: ActionsPluginSetup; alerts: AlertingPluginSetup; - spaces?: SpacesPluginSetup; - security?: SecurityPluginSetup; } export interface FixtureStartDeps { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; } export class FixturePlugin implements Plugin { - public setup( - core: CoreSetup, - { features, actions, alerts, spaces, security }: FixtureSetupDeps - ) { + public setup(core: CoreSetup, { features, actions, alerts }: FixtureSetupDeps) { features.registerKibanaFeature({ id: 'alertsFixture', name: 'Alerts', @@ -108,7 +105,7 @@ export class FixturePlugin implements Plugin, - { spaces, security }: Partial -) { +export function defineRoutes(core: CoreSetup) { const router = core.http.createRouter(); router.put( { @@ -40,13 +37,16 @@ export function defineRoutes( ): Promise> => { const { id } = req.params; + const [ + { savedObjects }, + { encryptedSavedObjects, security, spaces }, + ] = await core.getStartServices(); if (!security) { return res.ok({ body: {}, }); } - const [{ savedObjects }, { encryptedSavedObjects }] = await core.getStartServices(); const encryptedSavedObjectsWithAlerts = await encryptedSavedObjects.getClient({ includedHiddenTypes: ['alert'], }); @@ -70,7 +70,7 @@ export function defineRoutes( // Create an API key using the new grant API - in this case the Kibana system user is creating the // API key for the user, instead of having the user create it themselves, which requires api_key // privileges - const createAPIKeyResult = await security.authc.grantAPIKeyAsInternalUser(req, { + const createAPIKeyResult = await security.authc.apiKeys.grantAsInternalUser(req, { name: `alert:migrated-to-7.10:${user.username}`, role_descriptors: {}, }); diff --git a/x-pack/test/api_integration/apis/ml/results/get_anomalies_table_data.ts b/x-pack/test/api_integration/apis/ml/results/get_anomalies_table_data.ts index 0e5cf4488b065..a957073e755dd 100644 --- a/x-pack/test/api_integration/apis/ml/results/get_anomalies_table_data.ts +++ b/x-pack/test/api_integration/apis/ml/results/get_anomalies_table_data.ts @@ -55,9 +55,7 @@ export default ({ getService }: FtrProviderContext) => { await ml.api.cleanMlIndices(); }); - // Failing ES snapshot promotion after a ml-cpp change - // See https://github.com/elastic/kibana/issues/85363 - it.skip('should fetch anomalies table data', async () => { + it('should fetch anomalies table data', async () => { const requestBody = { jobIds: [JOB_CONFIG.job_id], criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }], @@ -78,7 +76,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(body.interval).to.eql('hour'); - expect(body.anomalies.length).to.eql(12); + expect(body.anomalies.length).to.eql(13); }); it('should validate request body', async () => { diff --git a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts index a399c07e31065..07e7cad89c24a 100644 --- a/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts +++ b/x-pack/test/api_integration/apis/security_solution/saved_objects/timeline.ts @@ -171,7 +171,7 @@ export default function ({ getService }: FtrProviderContext) { expect(kqlMode).to.be(timelineObject.kqlMode); expect(kqlQuery).to.eql(timelineObject.kqlQuery); expect(savedObjectId).to.not.be.empty(); - expect(sort).to.eql(timelineObject.sort); + expect(sort).to.eql([timelineObject.sort]); expect(title).to.be(timelineObject.title); expect(version).to.not.be.empty(); }); diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index d3f40188aa6d3..a04c2fef92329 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -15,250 +15,547 @@ const INDEX_NAME = 'filebeat-7.0.0-iot-2019.06'; const ID = 'QRhG1WgBqd-n62SwZYDT'; const EXPECTED_DATA = [ { - category: 'base', - field: '@timestamp', - values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + category: 'file', + field: 'file.path', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, - { category: '@version', field: '@version', values: ['1'], originalValue: '1' }, { - category: 'agent', - field: 'agent.ephemeral_id', - values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], - originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', + category: 'traefik', + field: 'traefik.access.geoip.region_iso_code', + values: ['US-WA'], + originalValue: ['US-WA'], }, { - category: 'agent', - field: 'agent.hostname', + category: 'host', + field: 'host.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { - category: 'agent', - field: 'agent.id', - values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], - originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', + category: 'traefik', + field: 'traefik.access.geoip.location', + values: ['{"long":-122.3341,"lat":47.6103}'], + originalValue: ['[{"coordinates":[-122.3341,47.6103],"type":"Point"}]'], + }, + { + category: 'suricata', + field: 'suricata.eve.src_port', + values: ['80'], + originalValue: ['80'], + }, + { + category: 'traefik', + field: 'traefik.access.geoip.city_name', + values: ['Seattle'], + originalValue: ['Seattle'], + }, + { + category: 'service', + field: 'service.type', + values: ['suricata'], + originalValue: ['suricata'], + }, + { + category: 'http', + field: 'http.request.method', + values: ['get'], + originalValue: ['get'], + }, + { + category: 'host', + field: 'host.os.version', + values: ['9 (stretch)'], + originalValue: ['9 (stretch)'], + }, + { + category: 'source', + field: 'source.geo.region_name', + values: ['Washington'], + originalValue: ['Washington'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.protocol', + values: ['HTTP/1.1'], + originalValue: ['HTTP/1.1'], + }, + { + category: 'host', + field: 'host.os.name', + values: ['Raspbian GNU/Linux'], + originalValue: ['Raspbian GNU/Linux'], + }, + { + category: 'source', + field: 'source.ip', + values: ['54.239.219.210'], + originalValue: ['54.239.219.210'], + }, + { + category: 'host', + field: 'host.name', + values: ['raspberrypi'], + originalValue: ['raspberrypi'], + }, + { + category: 'source', + field: 'source.geo.region_iso_code', + values: ['US-WA'], + originalValue: ['US-WA'], + }, + { + category: 'http', + field: 'http.response.status_code', + values: ['206'], + originalValue: ['206'], + }, + { + category: 'event', + field: 'event.kind', + values: ['event'], + originalValue: ['event'], + }, + { + category: 'suricata', + field: 'suricata.eve.flow_id', + values: ['196625917175466'], + originalValue: ['196625917175466'], + }, + { + category: 'source', + field: 'source.geo.city_name', + values: ['Seattle'], + originalValue: ['Seattle'], + }, + { + category: 'suricata', + field: 'suricata.eve.proto', + values: ['tcp'], + originalValue: ['tcp'], + }, + { + category: 'flow', + field: 'flow.locality', + values: ['public'], + originalValue: ['public'], + }, + { + category: 'traefik', + field: 'traefik.access.geoip.country_iso_code', + values: ['US'], + originalValue: ['US'], + }, + { + category: 'fileset', + field: 'fileset.name', + values: ['eve'], + originalValue: ['eve'], + }, + { + category: 'input', + field: 'input.type', + values: ['log'], + originalValue: ['log'], + }, + { + category: 'log', + field: 'log.offset', + values: ['1856288115'], + originalValue: ['1856288115'], }, - { category: 'agent', field: 'agent.type', values: ['filebeat'], originalValue: 'filebeat' }, - { category: 'agent', field: 'agent.version', values: ['7.0.0'], originalValue: '7.0.0' }, { category: 'destination', field: 'destination.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { - category: 'destination', - field: 'destination.ip', - values: ['10.100.7.196'], - originalValue: '10.100.7.196', + category: 'agent', + field: 'agent.hostname', + values: ['raspberrypi'], + originalValue: ['raspberrypi'], }, - { category: 'destination', field: 'destination.port', values: [40684], originalValue: 40684 }, - { category: 'ecs', field: 'ecs.version', values: ['1.0.0-beta2'], originalValue: '1.0.0-beta2' }, { - category: 'event', - field: 'event.dataset', - values: ['suricata.eve'], - originalValue: 'suricata.eve', + category: 'suricata', + field: 'suricata.eve.http.hostname', + values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { - category: 'event', - field: 'event.end', - values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + category: 'suricata', + field: 'suricata.eve.in_iface', + values: ['eth0'], + originalValue: ['eth0'], }, - { category: 'event', field: 'event.kind', values: ['event'], originalValue: 'event' }, - { category: 'event', field: 'event.module', values: ['suricata'], originalValue: 'suricata' }, - { category: 'event', field: 'event.type', values: ['fileinfo'], originalValue: 'fileinfo' }, { - category: 'file', - field: 'file.path', + category: 'base', + field: 'tags', + values: ['suricata'], + originalValue: ['suricata'], + }, + { + category: 'host', + field: 'host.architecture', + values: ['armv7l'], + originalValue: ['armv7l'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.status', + values: ['206'], + originalValue: ['206'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.url', values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, - { category: 'file', field: 'file.size', values: [48277], originalValue: 48277 }, - { category: 'fileset', field: 'fileset.name', values: ['eve'], originalValue: 'eve' }, - { category: 'flow', field: 'flow.locality', values: ['public'], originalValue: 'public' }, - { category: 'host', field: 'host.architecture', values: ['armv7l'], originalValue: 'armv7l' }, { - category: 'host', - field: 'host.hostname', - values: ['raspberrypi'], - originalValue: 'raspberrypi', + category: 'url', + field: 'url.path', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { - category: 'host', - field: 'host.id', - values: ['b19a781f683541a7a25ee345133aa399'], - originalValue: 'b19a781f683541a7a25ee345133aa399', + category: 'source', + field: 'source.port', + values: ['80'], + originalValue: ['80'], }, - { category: 'host', field: 'host.name', values: ['raspberrypi'], originalValue: 'raspberrypi' }, - { category: 'host', field: 'host.os.codename', values: ['stretch'], originalValue: 'stretch' }, - { category: 'host', field: 'host.os.family', values: [''], originalValue: '' }, { - category: 'host', - field: 'host.os.kernel', - values: ['4.14.50-v7+'], - originalValue: '4.14.50-v7+', + category: 'agent', + field: 'agent.id', + values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], + originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], }, { category: 'host', - field: 'host.os.name', - values: ['Raspbian GNU/Linux'], - originalValue: 'Raspbian GNU/Linux', + field: 'host.containerized', + values: ['false'], + originalValue: ['false'], + }, + { + category: 'ecs', + field: 'ecs.version', + values: ['1.0.0-beta2'], + originalValue: ['1.0.0-beta2'], + }, + { + category: 'agent', + field: 'agent.version', + values: ['7.0.0'], + originalValue: ['7.0.0'], + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.stored', + values: ['false'], + originalValue: ['false'], }, - { category: 'host', field: 'host.os.platform', values: ['raspbian'], originalValue: 'raspbian' }, { category: 'host', - field: 'host.os.version', - values: ['9 (stretch)'], - originalValue: '9 (stretch)', + field: 'host.os.family', + values: [''], + originalValue: [''], }, - { category: 'http', field: 'http.request.method', values: ['get'], originalValue: 'get' }, - { category: 'http', field: 'http.response.body.bytes', values: [48277], originalValue: 48277 }, - { category: 'http', field: 'http.response.status_code', values: [206], originalValue: 206 }, - { category: 'input', field: 'input.type', values: ['log'], originalValue: 'log' }, { category: 'base', field: 'labels.pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], - originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', + originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], }, { - category: 'log', - field: 'log.file.path', - values: ['/var/log/suricata/eve.json'], - originalValue: '/var/log/suricata/eve.json', + category: 'suricata', + field: 'suricata.eve.src_ip', + values: ['54.239.219.210'], + originalValue: ['54.239.219.210'], + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.state', + values: ['CLOSED'], + originalValue: ['CLOSED'], + }, + { + category: 'destination', + field: 'destination.port', + values: ['40684'], + originalValue: ['40684'], + }, + { + category: 'traefik', + field: 'traefik.access.geoip.region_name', + values: ['Washington'], + originalValue: ['Washington'], }, - { category: 'log', field: 'log.offset', values: [1856288115], originalValue: 1856288115 }, - { category: 'network', field: 'network.name', values: ['iot'], originalValue: 'iot' }, - { category: 'network', field: 'network.protocol', values: ['http'], originalValue: 'http' }, - { category: 'network', field: 'network.transport', values: ['tcp'], originalValue: 'tcp' }, - { category: 'service', field: 'service.type', values: ['suricata'], originalValue: 'suricata' }, - { category: 'source', field: 'source.as.num', values: [16509], originalValue: 16509 }, { category: 'source', - field: 'source.as.org', - values: ['Amazon.com, Inc.'], - originalValue: 'Amazon.com, Inc.', + field: 'source.as.num', + values: ['16509'], + originalValue: ['16509'], + }, + { + category: 'event', + field: 'event.end', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: ['2019-02-10T02:39:44.107Z'], + }, + { + category: 'source', + field: 'source.geo.location', + values: ['{"long":-122.3341,"lat":47.6103}'], + originalValue: ['[{"coordinates":[-122.3341,47.6103],"type":"Point"}]'], }, { category: 'source', field: 'source.domain', values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], - originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', + originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], }, { - category: 'source', - field: 'source.geo.city_name', - values: ['Seattle'], - originalValue: 'Seattle', + category: 'suricata', + field: 'suricata.eve.fileinfo.size', + values: ['48277'], + originalValue: ['48277'], }, { - category: 'source', - field: 'source.geo.continent_name', - values: ['North America'], - originalValue: 'North America', + category: 'suricata', + field: 'suricata.eve.app_proto', + values: ['http'], + originalValue: ['http'], }, - { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], originalValue: 'US' }, { - category: 'source', - field: 'source.geo.location.lat', - values: [47.6103], - originalValue: 47.6103, + category: 'agent', + field: 'agent.type', + values: ['filebeat'], + originalValue: ['filebeat'], }, { - category: 'source', - field: 'source.geo.location.lon', - values: [-122.3341], - originalValue: -122.3341, + category: 'suricata', + field: 'suricata.eve.fileinfo.tx_id', + values: ['301'], + originalValue: ['301'], }, { - category: 'source', - field: 'source.geo.region_iso_code', - values: ['US-WA'], - originalValue: 'US-WA', + category: 'event', + field: 'event.module', + values: ['suricata'], + originalValue: ['suricata'], + }, + { + category: 'network', + field: 'network.protocol', + values: ['http'], + originalValue: ['http'], + }, + { + category: 'host', + field: 'host.os.kernel', + values: ['4.14.50-v7+'], + originalValue: ['4.14.50-v7+'], }, { category: 'source', - field: 'source.geo.region_name', - values: ['Washington'], - originalValue: 'Washington', + field: 'source.geo.country_iso_code', + values: ['US'], + originalValue: ['US'], + }, + { + category: '@version', + field: '@version', + values: ['1'], + originalValue: ['1'], + }, + { + category: 'host', + field: 'host.id', + values: ['b19a781f683541a7a25ee345133aa399'], + originalValue: ['b19a781f683541a7a25ee345133aa399'], }, { category: 'source', - field: 'source.ip', - values: ['54.239.219.210'], - originalValue: '54.239.219.210', + field: 'source.as.org', + values: ['Amazon.com, Inc.'], + originalValue: ['Amazon.com, Inc.'], }, - { category: 'source', field: 'source.port', values: [80], originalValue: 80 }, { category: 'suricata', - field: 'suricata.eve.fileinfo.state', - values: ['CLOSED'], - originalValue: 'CLOSED', + field: 'suricata.eve.timestamp', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: ['2019-02-10T02:39:44.107Z'], }, - { category: 'suricata', field: 'suricata.eve.fileinfo.tx_id', values: [301], originalValue: 301 }, { - category: 'suricata', - field: 'suricata.eve.flow_id', - values: [196625917175466], - originalValue: 196625917175466, + category: 'host', + field: 'host.os.codename', + values: ['stretch'], + originalValue: ['stretch'], + }, + { + category: 'source', + field: 'source.geo.continent_name', + values: ['North America'], + originalValue: ['North America'], + }, + { + category: 'network', + field: 'network.name', + values: ['iot'], + originalValue: ['iot'], }, { category: 'suricata', - field: 'suricata.eve.http.http_content_type', - values: ['video/mp4'], - originalValue: 'video/mp4', + field: 'suricata.eve.http.http_method', + values: ['get'], + originalValue: ['get'], + }, + { + category: 'traefik', + field: 'traefik.access.geoip.continent_name', + values: ['North America'], + originalValue: ['North America'], + }, + { + category: 'file', + field: 'file.size', + values: ['48277'], + originalValue: ['48277'], + }, + { + category: 'destination', + field: 'destination.ip', + values: ['10.100.7.196'], + originalValue: ['10.100.7.196'], }, { category: 'suricata', - field: 'suricata.eve.http.protocol', - values: ['HTTP/1.1'], - originalValue: 'HTTP/1.1', + field: 'suricata.eve.http.length', + values: ['48277'], + originalValue: ['48277'], }, - { category: 'suricata', field: 'suricata.eve.in_iface', values: ['eth0'], originalValue: 'eth0' }, - { category: 'base', field: 'tags', values: ['suricata'], originalValue: ['suricata'] }, { - category: 'url', - field: 'url.domain', - values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + category: 'http', + field: 'http.response.body.bytes', + values: ['48277'], + originalValue: ['48277'], }, { - category: 'url', - field: 'url.original', + category: 'suricata', + field: 'suricata.eve.fileinfo.filename', values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + }, + { + category: 'suricata', + field: 'suricata.eve.dest_ip', + values: ['10.100.7.196'], + originalValue: ['10.100.7.196'], + }, + { + category: 'network', + field: 'network.transport', + values: ['tcp'], + originalValue: ['tcp'], }, { category: 'url', - field: 'url.path', + field: 'url.original', values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + }, + { + category: 'base', + field: '@timestamp', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: ['2019-02-10T02:39:44.107Z'], + }, + { + category: 'host', + field: 'host.os.platform', + values: ['raspbian'], + originalValue: ['raspbian'], + }, + { + category: 'suricata', + field: 'suricata.eve.dest_port', + values: ['40684'], + originalValue: ['40684'], + }, + { + category: 'event', + field: 'event.type', + values: ['fileinfo'], + originalValue: ['fileinfo'], + }, + { + category: 'log', + field: 'log.file.path', + values: ['/var/log/suricata/eve.json'], + originalValue: ['/var/log/suricata/eve.json'], + }, + { + category: 'url', + field: 'url.domain', + values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + }, + { + category: 'agent', + field: 'agent.ephemeral_id', + values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], + originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], + }, + { + category: 'suricata', + field: 'suricata.eve.http.http_content_type', + values: ['video/mp4'], + originalValue: ['video/mp4'], + }, + { + category: 'event', + field: 'event.dataset', + values: ['suricata.eve'], + originalValue: ['suricata.eve'], }, { category: '_index', field: '_index', values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: 'filebeat-7.0.0-iot-2019.06', + originalValue: ['filebeat-7.0.0-iot-2019.06'], }, { category: '_id', field: '_id', values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: 'QRhG1WgBqd-n62SwZYDT', + originalValue: ['QRhG1WgBqd-n62SwZYDT'], + }, + { + category: '_score', + field: '_score', + values: ['1'], + originalValue: ['1'], }, - { category: '_score', field: '_score', values: [1], originalValue: 1 }, ]; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts index 03ef521215219..29c081a6fe493 100644 --- a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts @@ -107,21 +107,42 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }, { req: { - url: `/api/apm/services/foo/transactions/charts?start=${start}&end=${end}&transactionType=bar&uiFilters=%7B%22environment%22%3A%22testing%22%7D`, + url: `/api/apm/services/foo/transactions/charts/latency?start=${start}&end=${end}&transactionType=bar&uiFilters=%7B%22environment%22%3A%22testing%22%7D`, }, expectForbidden: expect403, expectResponse: expect200, }, { req: { - url: `/api/apm/services/foo/transactions/charts?start=${start}&end=${end}&uiFilters=%7B%22environment%22%3A%22testing%22%7D`, + url: `/api/apm/services/foo/transactions/charts/latency?start=${start}&end=${end}&uiFilters=%7B%22environment%22%3A%22testing%22%7D`, }, expectForbidden: expect403, expectResponse: expect200, }, { req: { - url: `/api/apm/services/foo/transactions/charts?start=${start}&end=${end}&transactionType=bar&transactionName=baz&uiFilters=%7B%22environment%22%3A%22testing%22%7D`, + url: `/api/apm/services/foo/transactions/charts/latency?start=${start}&end=${end}&transactionType=bar&transactionName=baz&uiFilters=%7B%22environment%22%3A%22testing%22%7D`, + }, + expectForbidden: expect403, + expectResponse: expect200, + }, + { + req: { + url: `/api/apm/services/foo/transactions/charts/throughput?start=${start}&end=${end}&transactionType=bar&uiFilters=%7B%22environment%22%3A%22testing%22%7D`, + }, + expectForbidden: expect403, + expectResponse: expect200, + }, + { + req: { + url: `/api/apm/services/foo/transactions/charts/throughput?start=${start}&end=${end}&uiFilters=%7B%22environment%22%3A%22testing%22%7D`, + }, + expectForbidden: expect403, + expectResponse: expect200, + }, + { + req: { + url: `/api/apm/services/foo/transactions/charts/throughput?start=${start}&end=${end}&transactionType=bar&transactionName=baz&uiFilters=%7B%22environment%22%3A%22testing%22%7D`, }, expectForbidden: expect403, expectResponse: expect200, diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index f6ee79382dd07..3e625688e2459 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -26,6 +26,7 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont // TODO: we should not have a service overview. describe('Service overview', function () { loadTestFile(require.resolve('./service_overview/error_groups')); + loadTestFile(require.resolve('./service_overview/dependencies')); }); describe('Settings', function () { @@ -45,7 +46,8 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont describe('Transactions', function () { loadTestFile(require.resolve('./transactions/top_transaction_groups')); - loadTestFile(require.resolve('./transactions/transaction_charts')); + loadTestFile(require.resolve('./transactions/latency')); + loadTestFile(require.resolve('./transactions/throughput')); loadTestFile(require.resolve('./transactions/error_rate')); loadTestFile(require.resolve('./transactions/breakdown')); loadTestFile(require.resolve('./transactions/distribution')); diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/es_utils.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/es_utils.ts new file mode 100644 index 0000000000000..85f48d4c260ad --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/es_utils.ts @@ -0,0 +1,216 @@ +/* + * 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'; + +export function createServiceDependencyDocs({ + time, + service, + agentName, + resource, + responseTime, + outcome, + span, + to, +}: { + time: number; + resource: string; + responseTime: { + count: number; + sum: number; + }; + service: { + name: string; + environment?: string; + }; + agentName: string; + span: { + type: string; + subtype: string; + }; + outcome: 'success' | 'failure' | 'unknown'; + to?: { + service: { + name: string; + environment?: string; + }; + agentName: string; + }; +}) { + const spanId = uuid.v4(); + + return [ + { + processor: { + event: 'metric' as const, + }, + observer: { + version_major: 7, + }, + '@timestamp': new Date(time).toISOString(), + service, + agent: { + name: agentName, + }, + event: { + outcome, + }, + span: { + destination: { + service: { + resource, + response_time: { + sum: { + us: responseTime.sum, + }, + count: responseTime.count, + }, + }, + }, + }, + }, + { + processor: { + event: 'span' as const, + }, + observer: { + version_major: 7, + }, + '@timestamp': new Date(time).toISOString(), + service, + agent: { + name: agentName, + }, + event: { + outcome, + }, + span: { + destination: { + service: { + resource, + }, + }, + id: spanId, + type: span.type, + subtype: span.subtype, + }, + }, + ...(to + ? [ + { + processor: { + event: 'transaction' as const, + }, + observer: { + version_major: 7, + }, + '@timestamp': new Date(time + 1).toISOString(), + event: { + outcome: 'unknown', + }, + parent: { + id: spanId, + }, + service: to.service, + agent: { + name: to.agentName, + }, + }, + ] + : []), + ]; +} + +export const apmDependenciesMapping = { + properties: { + '@timestamp': { + type: 'date', + }, + event: { + dynamic: false, + properties: { + outcome: { + type: 'keyword', + }, + }, + }, + agent: { + dynamic: false, + properties: { + name: { + type: 'keyword', + }, + }, + }, + service: { + dynamic: false, + properties: { + name: { + type: 'keyword', + }, + environment: { + type: 'keyword', + }, + }, + }, + span: { + dynamic: false, + properties: { + id: { + type: 'keyword', + }, + type: { + type: 'keyword', + }, + subtype: { + type: 'keyword', + }, + destination: { + dynamic: false, + properties: { + service: { + dynamic: false, + properties: { + resource: { + type: 'keyword', + }, + response_time: { + properties: { + count: { + type: 'long', + }, + sum: { + properties: { + us: { + type: 'long', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + parent: { + dynamic: false, + properties: { + id: { + type: 'keyword', + }, + }, + }, + processor: { + dynamic: false, + properties: { + event: { + type: 'keyword', + }, + }, + }, + }, +}; diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/index.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/index.ts new file mode 100644 index 0000000000000..3349580f59068 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/dependencies/index.ts @@ -0,0 +1,385 @@ +/* + * 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 { sortBy, pick, last } from 'lodash'; +import { ValuesType } from 'utility-types'; +import { Maybe } from '../../../../../../plugins/apm/typings/common'; +import { isFiniteNumber } from '../../../../../../plugins/apm/common/utils/is_finite_number'; +import { APIReturnType } from '../../../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { ENVIRONMENT_ALL } from '../../../../../../plugins/apm/common/environment_filter_values'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import archives from '../../../../common/archives_metadata'; +import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils'; + +const round = (num: Maybe): string => (isFiniteNumber(num) ? num.toPrecision(4) : ''); + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + const archiveName = 'apm_8.0.0'; + const { start, end } = archives[archiveName]; + + describe('Service overview dependencies', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/dependencies`, + query: { + start, + end, + numBuckets: 20, + environment: ENVIRONMENT_ALL.value, + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + }); + + describe('when specific data is loaded', () => { + let response: { + status: number; + body: APIReturnType<'GET /api/apm/services/{serviceName}/dependencies'>; + }; + + const indices = { + metric: 'apm-dependencies-metric', + transaction: 'apm-dependencies-transaction', + span: 'apm-dependencies-span', + }; + + const startTime = new Date(start).getTime(); + const endTime = new Date(end).getTime(); + + after(async () => { + const allIndices = Object.values(indices).join(','); + const indexExists = (await es.indices.exists({ index: allIndices })).body; + if (indexExists) { + await es.indices.delete({ + index: allIndices, + }); + } + }); + + before(async () => { + await es.indices.create({ + index: indices.metric, + body: { + mappings: apmDependenciesMapping, + }, + }); + + await es.indices.create({ + index: indices.transaction, + body: { + mappings: apmDependenciesMapping, + }, + }); + + await es.indices.create({ + index: indices.span, + body: { + mappings: apmDependenciesMapping, + }, + }); + + const docs = [ + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'opbeans-node:3000', + outcome: 'success', + responseTime: { + count: 2, + sum: 10, + }, + time: startTime, + to: { + service: { + name: 'opbeans-node', + }, + agentName: 'nodejs', + }, + }), + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'opbeans-node:3000', + outcome: 'failure', + responseTime: { + count: 1, + sum: 10, + }, + time: startTime, + }), + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'postgres', + outcome: 'success', + responseTime: { + count: 1, + sum: 3, + }, + time: startTime, + }), + ...createServiceDependencyDocs({ + service: { + name: 'opbeans-java', + environment: 'production', + }, + agentName: 'java', + span: { + type: 'external', + subtype: 'http', + }, + resource: 'opbeans-node-via-proxy', + outcome: 'success', + responseTime: { + count: 1, + sum: 1, + }, + time: endTime - 1, + to: { + service: { + name: 'opbeans-node', + }, + agentName: 'nodejs', + }, + }), + ]; + + const bulkActions = docs.reduce( + (prev, doc) => { + return [...prev, { index: { _index: indices[doc.processor.event] } }, doc]; + }, + [] as Array< + | { + index: { + _index: string; + }; + } + | ValuesType + > + ); + + await es.bulk({ + body: bulkActions, + refresh: 'wait_for', + }); + + response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/dependencies`, + query: { + start, + end, + numBuckets: 20, + environment: ENVIRONMENT_ALL.value, + }, + }) + ); + }); + + it('returns a 200', () => { + expect(response.status).to.be(200); + }); + + it('returns two dependencies', () => { + expect(response.body.length).to.be(2); + }); + + it('returns opbeans-node as a dependency', () => { + const opbeansNode = response.body.find( + (item) => item.type === 'service' && item.serviceName === 'opbeans-node' + ); + + expect(opbeansNode !== undefined).to.be(true); + + const values = { + latency: round(opbeansNode?.latency.value), + throughput: round(opbeansNode?.throughput.value), + errorRate: round(opbeansNode?.errorRate.value), + ...pick(opbeansNode, 'serviceName', 'type', 'agentName', 'environment', 'impact'), + }; + + const count = 4; + const sum = 21; + const errors = 1; + + expect(values).to.eql({ + agentName: 'nodejs', + environment: '', + serviceName: 'opbeans-node', + type: 'service', + errorRate: round(errors / count), + latency: round(sum / count), + throughput: round(count / ((endTime - startTime) / 1000 / 60)), + impact: 100, + }); + + const firstValue = round(opbeansNode?.latency.timeseries[0].y); + const lastValue = round(last(opbeansNode?.latency.timeseries)?.y); + + expect(firstValue).to.be(round(20 / 3)); + expect(lastValue).to.be('1.000'); + }); + + it('returns postgres as an external dependency', () => { + const postgres = response.body.find( + (item) => item.type === 'external' && item.name === 'postgres' + ); + + expect(postgres !== undefined).to.be(true); + + const values = { + latency: round(postgres?.latency.value), + throughput: round(postgres?.throughput.value), + errorRate: round(postgres?.errorRate.value), + ...pick(postgres, 'spanType', 'spanSubtype', 'name', 'impact', 'type'), + }; + + const count = 1; + const sum = 3; + const errors = 0; + + expect(values).to.eql({ + spanType: 'external', + spanSubtype: 'http', + name: 'postgres', + type: 'external', + errorRate: round(errors / count), + latency: round(sum / count), + throughput: round(count / ((endTime - startTime) / 1000 / 60)), + impact: 0, + }); + }); + }); + + describe('when data is loaded', () => { + let response: { + status: number; + body: APIReturnType<'GET /api/apm/services/{serviceName}/dependencies'>; + }; + + before(async () => { + await esArchiver.load(archiveName); + + response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/dependencies`, + query: { + start, + end, + numBuckets: 20, + environment: ENVIRONMENT_ALL.value, + }, + }) + ); + }); + + after(() => esArchiver.unload(archiveName)); + + it('returns a successful response', () => { + expect(response.status).to.be(200); + }); + + it('returns at least one item', () => { + expect(response.body.length).to.be.greaterThan(0); + }); + + it('returns the right names', () => { + const names = response.body.map((item) => item.name); + expectSnapshot(names.sort()).toMatchInline(` + Array [ + "opbeans-go", + "postgresql", + ] + `); + }); + + it('returns the right service names', () => { + const serviceNames = response.body + .map((item) => (item.type === 'service' ? item.serviceName : undefined)) + .filter(Boolean); + + expectSnapshot(serviceNames.sort()).toMatchInline(` + Array [ + "opbeans-go", + ] + `); + }); + + it('returns the right latency values', () => { + const latencyValues = sortBy( + response.body.map((item) => ({ name: item.name, latency: item.latency.value })), + 'name' + ); + + expectSnapshot(latencyValues).toMatchInline(` + Array [ + Object { + "latency": 38506.4285714286, + "name": "opbeans-go", + }, + Object { + "latency": 5908.77272727273, + "name": "postgresql", + }, + ] + `); + }); + + it('returns the right throughput values', () => { + const throughputValues = sortBy( + response.body.map((item) => ({ name: item.name, latency: item.throughput.value })), + 'name' + ); + + expectSnapshot(throughputValues).toMatchInline(` + Array [ + Object { + "latency": 0.466666666666667, + "name": "opbeans-go", + }, + Object { + "latency": 3.66666666666667, + "name": "postgresql", + }, + ] + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index 03d5602d832ed..52c9dd74167f5 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { isEmpty, pick } from 'lodash'; -import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; +import { isEmpty, pick, sortBy } from 'lodash'; +import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import archives_metadata from '../../../common/archives_metadata'; @@ -43,11 +43,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(() => esArchiver.unload(archiveName)); describe('and fetching a list of services', () => { - let response: PromiseReturnType; + let response: { + status: number; + body: APIReturnType<'GET /api/apm/services'>; + }; + + let sortedItems: typeof response.body.items; + before(async () => { response = await supertest.get( `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` ); + sortedItems = sortBy(response.body.items, 'serviceName'); }); it('the response is successful', () => { @@ -63,16 +70,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the correct service names', () => { - expectSnapshot(response.body.items.map((item: any) => item.serviceName)).toMatchInline(` + expectSnapshot(sortedItems.map((item) => item.serviceName)).toMatchInline(` Array [ "kibana", - "opbeans-python", - "opbeans-node", - "opbeans-ruby", - "opbeans-go", "kibana-frontend", "opbeans-dotnet", + "opbeans-go", "opbeans-java", + "opbeans-node", + "opbeans-python", + "opbeans-ruby", "opbeans-rum", ] `); @@ -80,7 +87,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the correct metrics averages', () => { expectSnapshot( - response.body.items.map((item: any) => + sortedItems.map((item) => pick( item, 'transactionErrorRate.value', @@ -103,76 +110,76 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "avgResponseTime": Object { - "value": 217138.013645224, - }, - "transactionErrorRate": Object { - "value": 0.315789473684211, + "value": 2629229.16666667, }, "transactionsPerMinute": Object { - "value": 17.1, + "value": 3.2, }, }, Object { "avgResponseTime": Object { - "value": 563605.417040359, + "value": 631521.83908046, }, "transactionErrorRate": Object { - "value": 0.0210526315789474, + "value": 0.0229885057471264, }, "transactionsPerMinute": Object { - "value": 7.43333333333333, + "value": 2.9, }, }, Object { "avgResponseTime": Object { - "value": 70518.9328358209, + "value": 27946.1484375, }, "transactionErrorRate": Object { - "value": 0.0373134328358209, + "value": 0.015625, }, "transactionsPerMinute": Object { - "value": 4.46666666666667, + "value": 4.26666666666667, }, }, Object { "avgResponseTime": Object { - "value": 27946.1484375, + "value": 237339.813333333, }, "transactionErrorRate": Object { - "value": 0.015625, + "value": 0.16, }, "transactionsPerMinute": Object { - "value": 4.26666666666667, + "value": 2.5, }, }, Object { "avgResponseTime": Object { - "value": 2629229.16666667, + "value": 563605.417040359, + }, + "transactionErrorRate": Object { + "value": 0.0210526315789474, }, "transactionsPerMinute": Object { - "value": 3.2, + "value": 7.43333333333333, }, }, Object { "avgResponseTime": Object { - "value": 631521.83908046, + "value": 217138.013645224, }, "transactionErrorRate": Object { - "value": 0.0229885057471264, + "value": 0.315789473684211, }, "transactionsPerMinute": Object { - "value": 2.9, + "value": 17.1, }, }, Object { "avgResponseTime": Object { - "value": 237339.813333333, + "value": 70518.9328358209, }, "transactionErrorRate": Object { - "value": 0.16, + "value": 0.0373134328358209, }, "transactionsPerMinute": Object { - "value": 2.5, + "value": 4.46666666666667, }, }, Object { @@ -188,29 +195,28 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns environments', () => { - expectSnapshot(response.body.items.map((item: any) => item.environments ?? [])) - .toMatchInline(` + expectSnapshot(sortedItems.map((item) => item.environments ?? [])).toMatchInline(` Array [ Array [ "production", ], - Array [], Array [ - "testing", + "production", ], - Array [], Array [ - "testing", + "production", ], Array [ - "production", + "testing", ], Array [ "production", ], Array [ - "production", + "testing", ], + Array [], + Array [], Array [ "testing", ], @@ -222,22 +228,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { // RUM transactions don't have event.outcome set, // so they should not have an error rate - const rumServices = response.body.items.filter( - (item: any) => item.agentName === 'rum-js' - ); + const rumServices = sortedItems.filter((item) => item.agentName === 'rum-js'); expect(rumServices.length).to.be.greaterThan(0); - expect(rumServices.every((item: any) => isEmpty(item.transactionErrorRate?.value))); + expect(rumServices.every((item) => isEmpty(item.transactionErrorRate?.value))); }); it('non-RUM services all report transaction error rates', () => { - const nonRumServices = response.body.items.filter( - (item: any) => item.agentName !== 'rum-js' - ); + const nonRumServices = sortedItems.filter((item) => item.agentName !== 'rum-js'); expect( - nonRumServices.every((item: any) => { + nonRumServices.every((item) => { return ( typeof item.transactionErrorRate?.value === 'number' && item.transactionErrorRate.timeseries.length > 0 diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/transaction_charts.ts b/x-pack/test/apm_api_integration/basic/tests/transactions/latency.ts similarity index 50% rename from x-pack/test/apm_api_integration/basic/tests/transactions/transaction_charts.ts rename to x-pack/test/apm_api_integration/basic/tests/transactions/latency.ts index e10fa2145d2b5..d856483752ac3 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/transaction_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transactions/latency.ts @@ -20,20 +20,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { const end = encodeURIComponent(metadata.end); const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'testing' })); - describe('Transaction charts', () => { + describe('Latency', () => { describe('when data is not loaded ', () => { it('handles the empty state', async () => { const response = await supertest.get( - `/api/apm/services/opbeans-node/transactions/charts?start=${start}&end=${end}&uiFilters=${uiFilters}` + `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}` ); expect(response.status).to.be(200); - expect(response.body.apmTimeseries.overallAvgDuration).to.be(null); - expect(response.body.apmTimeseries.responseTimes.avg.length).to.be(0); - expect(response.body.apmTimeseries.responseTimes.p95.length).to.be(0); - expect(response.body.apmTimeseries.responseTimes.p99.length).to.be(0); - expect(response.body.apmTimeseries.tpmBuckets.length).to.be(0); + expect(response.body.overallAvgDuration).to.be(null); + expect(response.body.latencyTimeseries.avg.length).to.be(0); + expect(response.body.latencyTimeseries.p95.length).to.be(0); + expect(response.body.latencyTimeseries.p99.length).to.be(0); }); }); @@ -45,28 +44,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(async () => { response = await supertest.get( - `/api/apm/services/opbeans-node/transactions/charts?start=${start}&end=${end}&uiFilters=${uiFilters}` + `/api/apm/services/opbeans-node/transactions/charts/latency?start=${start}&end=${end}&uiFilters=${uiFilters}` ); }); - it('returns some data', async () => { + it('returns average duration and timeseries', async () => { expect(response.status).to.be(200); - expect(response.body.apmTimeseries.overallAvgDuration).not.to.be(null); - expect(response.body.apmTimeseries.responseTimes.avg.length).to.be.greaterThan(0); - expect(response.body.apmTimeseries.responseTimes.p95.length).to.be.greaterThan(0); - expect(response.body.apmTimeseries.responseTimes.p99.length).to.be.greaterThan(0); - expect(response.body.apmTimeseries.tpmBuckets.length).to.be.greaterThan(0); - }); - - it('returns the correct data', () => { - expectSnapshot(response.body.apmTimeseries.overallAvgDuration).toMatchInline( - `563605.417040359` - ); - expectSnapshot(response.body.apmTimeseries.responseTimes.avg.length).toMatchInline(`61`); - expectSnapshot(response.body.apmTimeseries.tpmBuckets.length).toMatchInline(`4`); - - expectSnapshot(response.body).toMatch(); + expect(response.body.overallAvgDuration).not.to.be(null); + expect(response.body.latencyTimeseries.avg.length).to.be.greaterThan(0); + expect(response.body.latencyTimeseries.p95.length).to.be.greaterThan(0); + expect(response.body.latencyTimeseries.p99.length).to.be.greaterThan(0); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/throughput.ts b/x-pack/test/apm_api_integration/basic/tests/transactions/throughput.ts new file mode 100644 index 0000000000000..beec346eb8d51 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transactions/throughput.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import archives_metadata from '../../../common/archives_metadata'; +import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + // url parameters + const start = encodeURIComponent(metadata.start); + const end = encodeURIComponent(metadata.end); + const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'testing' })); + + describe('Throughput', () => { + describe('when data is not loaded ', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-node/transactions/charts/throughput?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + + expect(response.status).to.be(200); + + expect(response.body.throughputTimeseries.length).to.be(0); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + let response: PromiseReturnType; + + before(async () => { + response = await supertest.get( + `/api/apm/services/opbeans-node/transactions/charts/throughput?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + }); + + it('returns throughput timeseries', async () => { + expect(response.status).to.be(200); + + expect(response.body.throughputTimeseries.length).to.be.greaterThan(0); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 30974125dba7d..262b0f2b0daaf 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -16,7 +16,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr }); describe('Transactions', function () { - loadTestFile(require.resolve('./transactions/transactions_charts.ts')); + loadTestFile(require.resolve('./transactions/latency')); }); describe('Settings', function () { diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index efd15df7e9c87..92f9a96136f11 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -5,6 +5,8 @@ */ import expect from '@kbn/expect'; +import { sortBy } from 'lodash'; +import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; import { PromiseReturnType } from '../../../../../plugins/observability/typings/common'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import archives_metadata from '../../../common/archives_metadata'; @@ -31,7 +33,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('with the default APM read user', () => { describe('and fetching a list of services', () => { - let response: PromiseReturnType; + let response: { + status: number; + body: APIReturnType<'GET /api/apm/services'>; + }; + before(async () => { response = await supertest.get( `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` @@ -54,7 +60,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { // services report as unknown (so without any health status): // https://github.com/elastic/kibana/issues/77083 - const healthStatuses = response.body.items.map((item: any) => item.healthStatus); + const healthStatuses = sortBy(response.body.items, 'serviceName').map( + (item: any) => item.healthStatus + ); expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0); diff --git a/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap b/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap new file mode 100644 index 0000000000000..1b7e2fbbc5a30 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Latency when data is loaded and fetching transaction charts with uiFilters when not defined environments seleted should return the correct anomaly boundaries 1`] = ` +Array [ + Object { + "x": 1607436000000, + "y": 0, + "y0": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + "y0": 0, + }, + Object { + "x": 1607437650000, + "y": 0, + "y0": 0, + }, +] +`; + +exports[`Latency when data is loaded and fetching transaction charts with uiFilters with environment selected and empty kuery filter should return a non-empty anomaly series 1`] = ` +Array [ + Object { + "x": 1607436000000, + "y": 1625128.56211579, + "y0": 7533.02707532227, + }, + Object { + "x": 1607436900000, + "y": 1660982.24115757, + "y0": 5732.00699123528, + }, + Object { + "x": 1607437650000, + "y": 1660982.24115757, + "y0": 5732.00699123528, + }, +] +`; + +exports[`Latency when data is loaded and fetching transaction charts with uiFilters with environment selected in uiFilters should return a non-empty anomaly series 1`] = ` +Array [ + Object { + "x": 1607436000000, + "y": 1625128.56211579, + "y0": 7533.02707532227, + }, + Object { + "x": 1607436900000, + "y": 1660982.24115757, + "y0": 5732.00699123528, + }, + Object { + "x": 1607437650000, + "y": 1660982.24115757, + "y0": 5732.00699123528, + }, +] +`; diff --git a/x-pack/test/apm_api_integration/trial/tests/transactions/transactions_charts.ts b/x-pack/test/apm_api_integration/trial/tests/transactions/latency.ts similarity index 90% rename from x-pack/test/apm_api_integration/trial/tests/transactions/transactions_charts.ts rename to x-pack/test/apm_api_integration/trial/tests/transactions/latency.ts index 08e2cb8a9e0c8..f248904facae3 100644 --- a/x-pack/test/apm_api_integration/trial/tests/transactions/transactions_charts.ts +++ b/x-pack/test/apm_api_integration/trial/tests/transactions/latency.ts @@ -22,7 +22,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const end = encodeURIComponent(range.end); const transactionType = 'request'; - describe('APM Transaction Overview', () => { + describe('Latency', () => { describe('when data is loaded', () => { before(() => esArchiver.load(archiveName)); after(() => esArchiver.unload(archiveName)); @@ -34,7 +34,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const uiFilters = encodeURIComponent(JSON.stringify({})); before(async () => { response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` ); }); it('should return an error response', () => { @@ -45,7 +45,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('without uiFilters', () => { before(async () => { response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts?start=${start}&end=${end}&transactionType=${transactionType}` + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}` ); }); it('should return an error response', () => { @@ -57,7 +57,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'production' })); before(async () => { response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` ); }); @@ -80,13 +80,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - describe('when not defined environments selected', () => { + describe('when not defined environments seleted', () => { const uiFilters = encodeURIComponent( JSON.stringify({ environment: 'ENVIRONMENT_NOT_DEFINED' }) ); before(async () => { response = await supertest.get( - `/api/apm/services/opbeans-python/transactions/charts?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` + `/api/apm/services/opbeans-python/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` ); }); @@ -112,7 +112,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const uiFilters = encodeURIComponent(JSON.stringify({ environment: 'ENVIRONMENT_ALL' })); before(async () => { response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` ); }); @@ -131,7 +131,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); before(async () => { response = await supertest.get( - `/api/apm/services/opbeans-java/transactions/charts?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` + `/api/apm/services/opbeans-java/transactions/charts/latency?start=${start}&end=${end}&transactionType=${transactionType}&uiFilters=${uiFilters}` ); }); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index b91399a4a6756..462b385f27e5d 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); - describe('lens smokescreen tests', () => { + // FLAKY: https://github.com/elastic/kibana/issues/77969 + describe.skip('lens smokescreen tests', () => { it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index a27317dccee8d..6934caea43c37 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -15,63 +15,81 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); describe('certificates', function () { - before(async () => { - await makeCheck({ es, tls: true }); - await uptime.goToRoot(true); - }); + describe('empty certificates', function () { + before(async () => { + await makeCheck({ es }); + await uptime.goToRoot(true); + }); - beforeEach(async () => { - await makeCheck({ es, tls: true }); + it('go to certs page', async () => { + await uptimeService.common.waitUntilDataIsLoaded(); + await uptimeService.cert.hasViewCertButton(); + await uptimeService.navigation.goToCertificates(); + }); + it('displays empty message', async () => { + await uptimeService.cert.displaysEmptyMessage(); + }); }); - it('can navigate to cert page', async () => { - await uptimeService.common.waitUntilDataIsLoaded(); - await uptimeService.cert.hasViewCertButton(); - await uptimeService.navigation.goToCertificates(); - }); + describe('with certs', function () { + before(async () => { + await makeCheck({ es, tls: true }); + await uptime.goToRoot(true); + }); - describe('page', () => { beforeEach(async () => { - await uptimeService.navigation.goToCertificates(); - await uptimeService.navigation.refreshApp(); + await makeCheck({ es, tls: true }); }); - it('displays certificates', async () => { - await uptimeService.cert.hasCertificates(); + it('can navigate to cert page', async () => { + await uptimeService.common.waitUntilDataIsLoaded(); + await uptimeService.cert.hasViewCertButton(); + await uptimeService.navigation.goToCertificates(); }); - it('displays specific certificates', async () => { - const certId = getSha256(); - const { monitorId } = await makeCheck({ - es, - tls: { - sha256: certId, - }, + describe('page', () => { + beforeEach(async () => { + await uptimeService.navigation.goToCertificates(); + await uptimeService.navigation.refreshApp(); }); - await uptimeService.navigation.refreshApp(); - await uptimeService.cert.certificateExists({ certId, monitorId }); - }); + it('displays certificates', async () => { + await uptimeService.cert.hasCertificates(); + }); + + it('displays specific certificates', async () => { + const certId = getSha256(); + const { monitorId } = await makeCheck({ + es, + tls: { + sha256: certId, + }, + }); + + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.certificateExists({ certId, monitorId }); + }); - it('performs search against monitor id', async () => { - const certId = getSha256(); - const { monitorId } = await makeCheck({ - es, - monitorId: 'cert-test-check-id', - fields: { - monitor: { - name: 'Cert Test Check', + it('performs search against monitor id', async () => { + const certId = getSha256(); + const { monitorId } = await makeCheck({ + es, + monitorId: 'cert-test-check-id', + fields: { + monitor: { + name: 'Cert Test Check', + }, + url: { + full: 'https://site-to-check.com/', + }, }, - url: { - full: 'https://site-to-check.com/', + tls: { + sha256: certId, }, - }, - tls: { - sha256: certId, - }, + }); + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.searchIsWorking(monitorId); }); - await uptimeService.navigation.refreshApp(); - await uptimeService.cert.searchIsWorking(monitorId); }); }); }); diff --git a/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz b/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz index e1b9c01101f6e..0000bc249b476 100644 Binary files a/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz and b/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/resolver/winlogbeat/data.json.gz b/x-pack/test/functional/es_archives/endpoint/resolver/winlogbeat/data.json.gz new file mode 100644 index 0000000000000..529ee42991f90 Binary files /dev/null and b/x-pack/test/functional/es_archives/endpoint/resolver/winlogbeat/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/resolver/winlogbeat/mappings.json b/x-pack/test/functional/es_archives/endpoint/resolver/winlogbeat/mappings.json new file mode 100644 index 0000000000000..a8673d85c3061 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/resolver/winlogbeat/mappings.json @@ -0,0 +1,2934 @@ +{ + "type": "index", + "value": { + "aliases": { + "winlogbeat-7.11.0-default": { + "is_write_index": true + } + }, + "index": "winlogbeat-7.11.0-2020.12.03-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "doc_values": false, + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1", + "routing": { + "allocation": { + "include": { + "_tier": "data_hot" + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/page_objects/monitoring_page.ts b/x-pack/test/functional/page_objects/monitoring_page.ts index bbfa884e71cda..afeb0542050c8 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.ts +++ b/x-pack/test/functional/page_objects/monitoring_page.ts @@ -10,7 +10,6 @@ export function MonitoringPageProvider({ getPageObjects, getService }: FtrProvid const PageObjects = getPageObjects(['common', 'header', 'security', 'login']); const testSubjects = getService('testSubjects'); const security = getService('security'); - const find = getService('find'); return new (class MonitoringPage { async navigateTo(useSuperUser = false) { @@ -28,11 +27,6 @@ export function MonitoringPageProvider({ getPageObjects, getService }: FtrProvid await PageObjects.common.navigateToApp('monitoring'); } - async getWelcome() { - const el = await find.byCssSelector('.euiCallOut--primary', 10000 * 10); - return await el.getVisibleText(); - } - async getAccessDeniedMessage() { return testSubjects.getVisibleText('accessDeniedTitle'); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index dfbd64813af9a..0a0a80e52de79 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -11,7 +11,8 @@ import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; -import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/data_frame_task_state'; +import { DataFrameTaskStateType } from '../../../../plugins/ml/common/types/data_frame_analytics'; +import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/common/constants/data_frame_analytics'; import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; import { JobType } from '../../../../plugins/ml/common/types/saved_objects'; export type MlApi = ProvidedType; @@ -245,7 +246,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return analyticsStats; }, - async getAnalyticsState(analyticsId: string): Promise { + async getAnalyticsState(analyticsId: string): Promise { log.debug(`Fetching analytics state for job ${analyticsId}`); const analyticsStats = await this.getDFAJobStats(analyticsId); @@ -254,7 +255,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { `Expected dataframe analytics stats to have exactly one object (got '${analyticsStats.data_frame_analytics.length}')` ); - const state: DATA_FRAME_TASK_STATE = analyticsStats.data_frame_analytics[0].state; + const state: DataFrameTaskStateType = analyticsStats.data_frame_analytics[0].state; return state; }, @@ -291,7 +292,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async waitForAnalyticsState( analyticsId: string, - expectedAnalyticsState: DATA_FRAME_TASK_STATE + expectedAnalyticsState: DataFrameTaskStateType ) { await retry.waitForWithTimeout( `analytics state to be ${expectedAnalyticsState}`, diff --git a/x-pack/test/functional/services/ml/data_frame_analytics.ts b/x-pack/test/functional/services/ml/data_frame_analytics.ts index 670e16ce4af94..268f9a15fa623 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlApi } from './api'; -import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/data_frame_task_state'; +import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/common/constants/data_frame_analytics'; export function MachineLearningDataFrameAnalyticsProvider( { getService }: FtrProviderContext, diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts index ab43604786282..fa1178bbb7453 100644 --- a/x-pack/test/functional/services/uptime/certificates.ts +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -59,5 +59,12 @@ export function UptimeCertProvider({ getService, getPageObjects }: FtrProviderCo await self.hasCertificates(1); }); }, + async displaysEmptyMessage() { + await testSubjects.existOrFail('uptimeCertsEmptyMessage'); + const emptyText = await testSubjects.getVisibleText('uptimeCertsEmptyMessage'); + expect(emptyText).to.eql( + 'No Certificates found. Note: Certificates are only visible for Heartbeat 7.8+' + ); + }, }; } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts index 8dc78ed71d0b6..76361bd459890 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts @@ -106,10 +106,21 @@ export default function ({ getService }: FtrProviderContext) { // this id comes from the es archive file endpoint/pipeline/dns const id = 'LrLSOVHVsFY94TAi++++++eF'; const { body }: { body: ResolverPaginatedEvents } = await supertest - .post(`/api/endpoint/resolver/events?limit=1`) + .post(`/api/endpoint/resolver/events`) + .query({ limit: 1 }) .set('kbn-xsrf', 'xxx') .send({ - filter: `event.id:"${id}"`, + filter: JSON.stringify({ + bool: { + filter: [{ term: { 'event.id': id } }], + }, + }), + indexPatterns: [eventsIndexPattern], + // these times are taken from the es archiver data endpoint/pipeline/dns for this specific event + timeRange: { + from: '2020-10-01T13:50:15.14364600Z', + to: '2020-10-01T13:50:15.14364600Z', + }, }) .expect(200); expect(body.events.length).to.eql(1); @@ -121,10 +132,21 @@ export default function ({ getService }: FtrProviderContext) { // this id comes from the es archive file endpoint/pipeline/dns const id = 'LrLSOVHVsFY94TAi++++++eP'; const { body }: { body: ResolverPaginatedEvents } = await supertest - .post(`/api/endpoint/resolver/events?limit=1`) + .post(`/api/endpoint/resolver/events`) + .query({ limit: 1 }) .set('kbn-xsrf', 'xxx') .send({ - filter: `event.id:"${id}"`, + filter: JSON.stringify({ + bool: { + filter: [{ term: { 'event.id': id } }], + }, + }), + indexPatterns: [eventsIndexPattern], + // these times are taken from the es archiver data endpoint/pipeline/dns for this specific event + timeRange: { + from: '2020-10-01T13:50:15.44516300Z', + to: '2020-10-01T13:50:15.44516300Z', + }, }) .expect(200); expect(body.events.length).to.eql(1); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts index 2607b934e7df2..f26e2410b6c55 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts @@ -13,52 +13,93 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('Resolver tests for the entity route', () => { - before(async () => { - await esArchiver.load('endpoint/resolver/signals'); - }); + describe('winlogbeat tests', () => { + before(async () => { + await esArchiver.load('endpoint/resolver/winlogbeat'); + }); - after(async () => { - await esArchiver.unload('endpoint/resolver/signals'); - }); + after(async () => { + await esArchiver.unload('endpoint/resolver/winlogbeat'); + }); - it('returns an event even if it does not have a mapping for entity_id', async () => { - // this id is from the es archive - const _id = 'fa7eb1546f44fd47d8868be8d74e0082e19f22df493c67a7725457978eb648ab'; - const { body }: { body: ResolverEntityIndex } = await supertest.get( - `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` - ); - expect(body).eql([ - { - name: 'endpoint', - schema: { - id: 'process.entity_id', - parent: 'process.parent.entity_id', - ancestry: 'process.Ext.ancestry', - name: 'process.name', + it('returns a winlogbeat sysmon event when the event matches the schema correctly', async () => { + // this id is from the es archive + const _id = 'sysmon-event'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=winlogbeat-7.11.0-default` + ); + expect(body).eql([ + { + name: 'winlogbeat', + schema: { + id: 'process.entity_id', + parent: 'process.parent.entity_id', + name: 'process.name', + }, + // this value is from the es archive + id: '{98da333e-2060-5fc9-2e01-000000003f00}', }, - // this value is from the es archive - id: - 'MTIwNWY1NWQtODRkYS00MzkxLWIyNWQtYTNkNGJmNDBmY2E1LTc1NTItMTMyNDM1NDY1MTQuNjI0MjgxMDA=', - }, - ]); - }); + ]); + }); - it('does not return an event when it does not have the entity_id field in the document', async () => { - // this id is from the es archive - const _id = 'no-entity-id-field'; - const { body }: { body: ResolverEntityIndex } = await supertest.get( - `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` - ); - expect(body).to.be.empty(); + it('does not return a powershell event that has event.module set to powershell', async () => { + // this id is from the es archive + const _id = 'powershell-event'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=winlogbeat-7.11.0-default` + ); + expect(body).to.be.empty(); + }); }); - it('does not return an event when it does not have the process field in the document', async () => { - // this id is from the es archive - const _id = 'no-process-field'; - const { body }: { body: ResolverEntityIndex } = await supertest.get( - `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` - ); - expect(body).to.be.empty(); + describe('signals index mapping tests', () => { + before(async () => { + await esArchiver.load('endpoint/resolver/signals'); + }); + + after(async () => { + await esArchiver.unload('endpoint/resolver/signals'); + }); + + it('returns an event even if it does not have a mapping for entity_id', async () => { + // this id is from the es archive + const _id = 'fa7eb1546f44fd47d8868be8d74e0082e19f22df493c67a7725457978eb648ab'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` + ); + expect(body).eql([ + { + name: 'endpoint', + schema: { + id: 'process.entity_id', + parent: 'process.parent.entity_id', + ancestry: 'process.Ext.ancestry', + name: 'process.name', + }, + // this value is from the es archive + id: + 'MTIwNWY1NWQtODRkYS00MzkxLWIyNWQtYTNkNGJmNDBmY2E1LTc1NTItMTMyNDM1NDY1MTQuNjI0MjgxMDA=', + }, + ]); + }); + + it('does not return an event when it does not have the entity_id field in the document', async () => { + // this id is from the es archive + const _id = 'no-entity-id-field'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` + ); + expect(body).to.be.empty(); + }); + + it('does not return an event when it does not have the process field in the document', async () => { + // this id is from the es archive + const _id = 'no-process-field'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` + ); + expect(body).to.be.empty(); + }); }); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts index 0878c09cff500..220d932787fff 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { eventIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { JsonObject } from 'src/plugins/kibana_utils/common'; +import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; +import { + eventIDSafeVersion, + parentEntityIDSafeVersion, + timestampAsDateSafeVersion, +} from '../../../../plugins/security_solution/common/endpoint/models/event'; import { ResolverPaginatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { @@ -41,23 +47,42 @@ export default function ({ getService }: FtrProviderContext) { }; describe('event route', () => { + let entityIDFilterArray: JsonObject[] | undefined; let entityIDFilter: string | undefined; before(async () => { resolverTrees = await resolver.createTrees(treeOptions); // we only requested a single alert so there's only 1 tree tree = resolverTrees.trees[0]; - entityIDFilter = `process.entity_id:"${tree.origin.id}" and not event.category:"process"`; + entityIDFilterArray = [ + { term: { 'process.entity_id': tree.origin.id } }, + { bool: { must_not: { term: { 'event.category': 'process' } } } }, + ]; + entityIDFilter = JSON.stringify({ + bool: { + filter: entityIDFilterArray, + }, + }); }); after(async () => { await resolver.deleteData(resolverTrees); }); it('should filter events by event.id', async () => { + const filter = JSON.stringify({ + bool: { + filter: [{ term: { 'event.id': tree.origin.relatedEvents[0]?.event?.id } }], + }, + }); const { body }: { body: ResolverPaginatedEvents } = await supertest .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') .send({ - filter: `event.id:"${tree.origin.relatedEvents[0]?.event?.id}"`, + filter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, }) .expect(200); expect(body.events.length).to.eql(1); @@ -66,11 +91,21 @@ export default function ({ getService }: FtrProviderContext) { }); it('should not find any events when given an invalid entity id', async () => { + const filter = JSON.stringify({ + bool: { + filter: [{ term: { 'process.entity_id': '5555' } }], + }, + }); const { body }: { body: ResolverPaginatedEvents } = await supertest .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') .send({ - filter: 'process.entity_id:"5555"', + filter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, }) .expect(200); expect(body.nextEvent).to.eql(null); @@ -83,6 +118,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filter: entityIDFilter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, }) .expect(200); expect(body.events.length).to.eql(4); @@ -91,12 +131,24 @@ export default function ({ getService }: FtrProviderContext) { }); it('should allow for the events to be filtered', async () => { - const filter = `event.category:"${RelatedEventCategory.Driver}" and ${entityIDFilter}`; + const filter = JSON.stringify({ + bool: { + filter: [ + { term: { 'event.category': RelatedEventCategory.Driver } }, + ...(entityIDFilterArray ?? []), + ], + }, + }); const { body }: { body: ResolverPaginatedEvents } = await supertest .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') .send({ filter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, }) .expect(200); expect(body.events.length).to.eql(2); @@ -113,6 +165,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filter: entityIDFilter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, }) .expect(200); expect(body.events.length).to.eql(2); @@ -124,6 +181,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filter: entityIDFilter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, }) .expect(200)); expect(body.events.length).to.eql(2); @@ -135,6 +197,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filter: entityIDFilter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, }) .expect(200)); expect(body.events).to.be.empty(); @@ -147,6 +214,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filter: entityIDFilter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, }) .expect(200); expect(body.events.length).to.eql(4); @@ -160,6 +232,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filter: entityIDFilter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, }) .expect(200); expect(body.events.length).to.eql(4); @@ -171,5 +248,122 @@ export default function ({ getService }: FtrProviderContext) { expect(eventIDSafeVersion(body.events[i])).to.equal(relatedEvents[i].event?.id); } }); + + it('should only return data within the specified timeRange', async () => { + const from = + timestampAsDateSafeVersion(tree.origin.relatedEvents[0])?.toISOString() ?? + new Date(0).toISOString(); + const to = from; + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + indexPatterns: [eventsIndexPattern], + timeRange: { + from, + to, + }, + }) + .expect(200); + expect(body.events.length).to.eql(1); + expect(tree.origin.relatedEvents[0]?.event?.id).to.eql(body.events[0].event?.id); + expect(body.nextEvent).to.eql(null); + }); + + it('should not find events when using an incorrect index pattern', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + indexPatterns: ['metrics-*'], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, + }) + .expect(200); + expect(body.events.length).to.eql(0); + expect(body.nextEvent).to.eql(null); + }); + + it('should retrieve lifecycle events for multiple ids', async () => { + const originParentID = parentEntityIDSafeVersion(tree.origin.lifecycle[0]) ?? ''; + expect(originParentID).to.not.be(''); + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter: JSON.stringify({ + bool: { + filter: [ + { terms: { 'process.entity_id': [tree.origin.id, originParentID] } }, + { term: { 'event.category': 'process' } }, + ], + }, + }), + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, + }) + .expect(200); + // 2 lifecycle events for the origin and 2 for the origin's parent + expect(body.events.length).to.eql(4); + expect(body.nextEvent).to.eql(null); + }); + + it('should paginate lifecycle events for multiple ids', async () => { + const originParentID = parentEntityIDSafeVersion(tree.origin.lifecycle[0]) ?? ''; + expect(originParentID).to.not.be(''); + let { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) + .query({ limit: 2 }) + .set('kbn-xsrf', 'xxx') + .send({ + filter: JSON.stringify({ + bool: { + filter: [ + { terms: { 'process.entity_id': [tree.origin.id, originParentID] } }, + { term: { 'event.category': 'process' } }, + ], + }, + }), + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, + }) + .expect(200); + expect(body.events.length).to.eql(2); + expect(body.nextEvent).not.to.eql(null); + + ({ body } = await supertest + .post(`/api/endpoint/resolver/events`) + .query({ limit: 3, afterEvent: body.nextEvent }) + .set('kbn-xsrf', 'xxx') + .send({ + filter: JSON.stringify({ + bool: { + filter: [ + { terms: { 'process.entity_id': [tree.origin.id, originParentID] } }, + { term: { 'event.category': 'process' } }, + ], + }, + }), + indexPatterns: [eventsIndexPattern], + timeRange: { + from: tree.startTime, + to: tree.endTime, + }, + }) + .expect(200)); + + expect(body.events.length).to.eql(2); + expect(body.nextEvent).to.eql(null); + }); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 7a1210c6b762f..9a731f1d5aee0 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -84,7 +84,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 9, schema: schemaWithAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -109,7 +109,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 9, schema: schemaWithAncestry, nodes: ['bogus id'], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -130,7 +130,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 3, schema: schemaWithAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -155,7 +155,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 50, schema: schemaWithoutAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -183,7 +183,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 50, schema: schemaWithoutAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from, to: from, }, @@ -210,7 +210,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 50, schema: schemaWithoutAncestry, nodes: [tree.origin.id, bottomMostDescendant], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -246,7 +246,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 50, schema: schemaWithoutAncestry, nodes: [leftNode, rightNode], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -277,7 +277,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 50, schema: schemaWithoutAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -299,7 +299,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 0, schema: schemaWithoutAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -329,7 +329,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 0, schema: schemaWithAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -358,7 +358,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 0, schema: schemaWithAncestry, nodes: ['bogus id'], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -381,7 +381,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 0, schema: schemaWithoutAncestry, nodes: [childID], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -414,7 +414,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 0, schema: schemaWithAncestry, nodes: [leftNodeID, rightNodeID], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -447,7 +447,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 0, schema: schemaWithAncestry, nodes: [tree.origin.id, originGrandparent], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -484,7 +484,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 0, schema: schemaWithoutAncestry, nodes: [tree.origin.id, originGrandparent], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -520,7 +520,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 0, schema: schemaWithoutAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: end, }, @@ -549,7 +549,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 50, schema: schemaWithName, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -587,7 +587,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 50, schema: schemaWithoutAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -625,7 +625,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 50, schema: schemaWithAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, @@ -663,7 +663,7 @@ export default function ({ getService }: FtrProviderContext) { ancestors: 0, schema: schemaWithAncestry, nodes: [tree.origin.id], - timerange: { + timeRange: { from: tree.startTime.toISOString(), to: tree.endTime.toISOString(), }, diff --git a/x-pack/test/stack_functional_integration/apps/apm/apm_smoke_test.js b/x-pack/test/stack_functional_integration/apps/apm/apm_smoke_test.js new file mode 100644 index 0000000000000..2e0d6f26d190b --- /dev/null +++ b/x-pack/test/stack_functional_integration/apps/apm/apm_smoke_test.js @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ getService, getPageObjects }) { + describe('APM smoke test', function ampsmokeTest() { + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'timePicker']); + const find = getService('find'); + const log = getService('log'); + + before(async () => { + await browser.setWindowSize(1200, 800); + await PageObjects.common.navigateToApp('apm'); + await PageObjects.timePicker.setCommonlyUsedTime('Last_1 year'); + }); + + it('can navigate to APM app', async () => { + await testSubjects.existOrFail('apmMainContainer', { + timeout: 10000, + }); + await find.clickByLinkText('apm-a-rum-test-e2e-general-usecase'); + log.debug('### apm smoke test passed'); + await find.clickByLinkText('general-usecase-initial-p-load'); + log.debug('### general use case smoke test passed'); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/apps/apm/index.js b/x-pack/test/stack_functional_integration/apps/apm/index.js new file mode 100644 index 0000000000000..3cf11decd3ebe --- /dev/null +++ b/x-pack/test/stack_functional_integration/apps/apm/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default ({ loadTestFile }) => { + describe('APM smoke test', function () { + loadTestFile(require.resolve('./apm_smoke_test')); + }); +}; diff --git a/x-pack/test/stack_functional_integration/apps/ml/index.js b/x-pack/test/stack_functional_integration/apps/ml/index.js new file mode 100644 index 0000000000000..257b5838f369f --- /dev/null +++ b/x-pack/test/stack_functional_integration/apps/ml/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default ({ loadTestFile }) => { + describe('Machine Learning', function () { + loadTestFile(require.resolve('./ml_smoke_test')); + }); +}; diff --git a/x-pack/test/stack_functional_integration/apps/ml/ml_smoke_test.js b/x-pack/test/stack_functional_integration/apps/ml/ml_smoke_test.js new file mode 100644 index 0000000000000..feaaefeea2e15 --- /dev/null +++ b/x-pack/test/stack_functional_integration/apps/ml/ml_smoke_test.js @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ getService }) { + describe('ML smoke test should check all the tabs', function mlSmokeTest() { + const browser = getService('browser'); + const ml = getService('ml'); + + before(async () => { + await browser.setWindowSize(1200, 800); + await ml.navigation.navigateToMl(); + }); + + it('should display tabs in the ML app correctly', async () => { + await ml.testExecution.logTestStep('should load the ML app'); + await ml.navigation.navigateToMl(); + + await ml.testExecution.logTestStep('should display the enabled "Overview" tab'); + await ml.navigation.assertOverviewTabEnabled(true); + + await ml.testExecution.logTestStep('should display the enabled "Anomaly Detection" tab'); + await ml.navigation.assertAnomalyDetectionTabEnabled(true); + + await ml.testExecution.logTestStep('should display the enabled "Data Frame Analytics" tab'); + await ml.navigation.assertDataFrameAnalyticsTabEnabled(true); + + await ml.testExecution.logTestStep('should display the enabled "Data Visualizer" tab'); + await ml.navigation.assertDataVisualizerTabEnabled(true); + + await ml.testExecution.logTestStep('should display the enabled "Settings" tab'); + await ml.navigation.assertSettingsTabEnabled(true); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js index 5cfc88ec9bce1..46112f59bbcac 100644 --- a/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js +++ b/x-pack/test/stack_functional_integration/apps/telemetry/_telemetry.js @@ -9,19 +9,18 @@ import expect from '@kbn/expect'; export default ({ getService, getPageObjects }) => { const log = getService('log'); const browser = getService('browser'); - const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'monitoring', 'header']); + const PageObjects = getPageObjects(['common']); describe('telemetry', function () { before(async () => { log.debug('monitoring'); await browser.setWindowSize(1200, 800); - await appsMenu.clickLink('Stack Monitoring'); + await PageObjects.common.navigateToApp('home'); }); it('should show banner Help us improve the Elastic Stack', async () => { - const actualMessage = await PageObjects.monitoring.getWelcome(); - log.debug(`X-Pack message = ${actualMessage}`); + const actualMessage = await PageObjects.common.getWelcomeText(); + log.debug(`### X-Pack Welcome Text: ${actualMessage}`); expect(actualMessage).to.contain('Help us improve the Elastic Stack'); }); }); diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index c63d85bd82dc0..acd36b0a78127 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -5,6 +5,7 @@ */ import { Unionize, UnionToIntersection } from 'utility-types'; +import { ESSearchHit, MaybeReadonlyArray, ESSourceOptions, ESHitsOf } from '.'; type SortOrder = 'asc' | 'desc'; type SortInstruction = Record; @@ -21,8 +22,6 @@ type Script = type BucketsPath = string | Record; -type SourceOptions = string | string[]; - type AggregationSourceOptions = | { field: string; @@ -104,7 +103,9 @@ export interface AggregationOptionsByType { from?: number; size?: number; sort?: SortOptions; - _source?: SourceOptions; + _source?: ESSourceOptions; + fields?: MaybeReadonlyArray; + docvalue_fields?: MaybeReadonlyArray; }; filter: Record; filters: { @@ -178,6 +179,10 @@ export interface AggregationOptionsByType { }; script: string; }; + top_metrics: { + metrics: { field: string } | MaybeReadonlyArray<{ field: string }>; + sort: SortOptions; + }; } type AggregationType = keyof AggregationOptionsByType; @@ -271,9 +276,9 @@ interface AggregationResponsePart; + hits: TAggregationOptionsMap extends { top_hits: AggregationOptionsByType['top_hits'] } + ? ESHitsOf + : ESSearchHit[]; }; }; filter: { @@ -369,8 +374,28 @@ interface AggregationResponsePart + : TAggregationOptionsMap extends { + top_metrics: { metrics: MaybeReadonlyArray<{ field: infer TFieldName }> }; + } + ? TopMetricsMap + : TopMetricsMap + >; + } + ]; } +type TopMetricsMap = TFieldName extends string + ? Record + : Record; + // Type for debugging purposes. If you see an error in AggregationResponseMap // similar to "cannot be used to index type", uncomment the type below and hover // over it to see what aggregation response types are missing compared to the diff --git a/x-pack/typings/elasticsearch/index.d.ts b/x-pack/typings/elasticsearch/index.d.ts index f2c51f601a099..ff20ce39d6446 100644 --- a/x-pack/typings/elasticsearch/index.d.ts +++ b/x-pack/typings/elasticsearch/index.d.ts @@ -3,8 +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 { SearchParams, SearchResponse } from 'elasticsearch'; +import { ValuesType } from 'utility-types'; +import { Explanation, SearchParams, SearchResponse } from 'elasticsearch'; import { AggregationResponseMap, AggregationInputMap, SortOptions } from './aggregations'; export { AggregationInputMap, @@ -18,15 +18,21 @@ export { // Typings for Elasticsearch queries and aggregations. These are intended to be // moved to the Elasticsearch JS client at some point (see #77720.) +export type MaybeReadonlyArray = T[] | readonly T[]; + interface CollapseQuery { field: string; inner_hits: { name: string; size?: number; sort?: SortOptions; - _source?: { - includes: string[]; - }; + _source?: + | string + | string[] + | { + includes?: string | string[]; + excludes?: string | string[]; + }; collapse?: { field: string; }; @@ -34,6 +40,28 @@ interface CollapseQuery { max_concurrent_group_searches?: number; } +export type ESSourceOptions = boolean | string | string[]; + +export type ESHitsOf< + TOptions extends + | { + size?: number; + _source?: ESSourceOptions; + docvalue_fields?: MaybeReadonlyArray; + fields?: MaybeReadonlyArray; + } + | undefined, + TDocument extends unknown +> = Array< + ESSearchHit< + TOptions extends { _source: false } ? undefined : TDocument, + TOptions extends { fields: MaybeReadonlyArray } ? TOptions['fields'] : undefined, + TOptions extends { docvalue_fields: MaybeReadonlyArray } + ? TOptions['docvalue_fields'] + : undefined + > +>; + export interface ESSearchBody { query?: any; size?: number; @@ -41,7 +69,7 @@ export interface ESSearchBody { aggs?: AggregationInputMap; track_total_hits?: boolean | number; collapse?: CollapseQuery; - _source?: string | string[] | { excludes: string | string[] }; + _source?: ESSourceOptions; } export type ESSearchRequest = Omit & { @@ -52,7 +80,32 @@ export interface ESSearchOptions { restTotalHitsAsInt: boolean; } -export type ESSearchHit = SearchResponse['hits']['hits'][0]; +export type ESSearchHit< + TSource extends any = unknown, + TFields extends MaybeReadonlyArray | undefined = undefined, + TDocValueFields extends MaybeReadonlyArray | undefined = undefined +> = { + _index: string; + _type: string; + _id: string; + _score: number; + _version?: number; + _explanation?: Explanation; + highlight?: any; + inner_hits?: any; + matched_queries?: string[]; + sort?: string[]; +} & (TSource extends false ? {} : { _source: TSource }) & + (TFields extends MaybeReadonlyArray + ? { + fields: Partial, unknown[]>>; + } + : {}) & + (TDocValueFields extends MaybeReadonlyArray + ? { + fields: Partial, unknown[]>>; + } + : {}); export type ESSearchResponse< TDocument, @@ -64,7 +117,7 @@ export type ESSearchResponse< aggregations?: AggregationResponseMap; } : {}) & { - hits: Omit['hits'], 'total'> & + hits: Omit['hits'], 'total' | 'hits'> & (TOptions['restTotalHitsAsInt'] extends true ? { total: number; @@ -74,7 +127,7 @@ export type ESSearchResponse< value: number; relation: 'eq' | 'gte'; }; - }); + }) & { hits: ESHitsOf }; }; export interface ESFilter { diff --git a/yarn.lock b/yarn.lock index 3ab7a4a776a7b..789ff171ef9c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1381,10 +1381,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@24.3.0": - version "24.3.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-24.3.0.tgz#5bb62143c2f941becbbbf91aafde849034b6330f" - integrity sha512-CmyekVOdy242m9pYf2yBNA6d54b8cohmNeoWghtNkM2wHT8Ut856zPV7mRhAMgNG61I7/pNCEnCD0OOpZPr4Xw== +"@elastic/charts@24.4.0": + version "24.4.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-24.4.0.tgz#217f55540f48a8f59c49250781d99c36110b2544" + integrity sha512-8dxDEs0g1mV4MjPgIArAmdDQDKjH8EitCLh8/Rouv8kkxvdXnL86VkSHpUbZNK9zPAecArwHBSkyCBZNmbqT2A== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -7433,11 +7433,6 @@ async-done@^1.2.0, async-done@^1.2.2: process-nextick-args "^2.0.0" stream-exhaust "^1.0.1" -async-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - integrity sha1-GdOGodntxufByF04iu28xW0zYC0= - async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" @@ -8258,11 +8253,6 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -binary-extensions@^1.0.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" - integrity sha1-RqoXUftqL5PuXmibsQh9SxTGwgU= - binary-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" @@ -8273,13 +8263,6 @@ binary-search@^1.3.3: resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - bl@^4.0.1, bl@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" @@ -8426,7 +8409,7 @@ brace@0.11.1, brace@^0.11.0, brace@^0.11.1: resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg= -braces@^2.3.1, braces@^2.3.2: +braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== @@ -9341,63 +9324,10 @@ cheerio@^1.0.0-rc.3: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058" - integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.0" - optionalDependencies: - fsevents "^1.2.7" - -chokidar@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" - integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== - 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.2.0" - optionalDependencies: - fsevents "~2.1.1" - -chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.1, chokidar@^2.1.2, chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.4.1, chokidar@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" - integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== +chokidar@2.1.2, chokidar@3.3.0, chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.1, chokidar@^2.1.2, chokidar@^2.1.8, chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.4.1, chokidar@^3.4.3: + 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" @@ -9405,7 +9335,7 @@ chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.4.1, chokidar@^3.4.2: is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.4.0" + readdirp "~3.5.0" optionalDependencies: fsevents "~2.1.2" @@ -13636,11 +13566,6 @@ file-type@^9.0.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-9.0.0.tgz#a68d5ad07f486414dfb2c8866f73161946714a18" integrity sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw== -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - filelist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.1.tgz#f10d1a3ae86c1694808e8f20906f43d4c9132dbb" @@ -14208,15 +14133,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^1.2.7: - version "1.2.12" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c" - integrity sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@^2.1.2, fsevents@~2.1.1, fsevents@~2.1.2: +fsevents@^2.1.2, fsevents@~2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== @@ -15213,13 +15130,6 @@ grunt-peg@^2.0.1: dependencies: pegjs "~0.9.0" -grunt-run@0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/grunt-run/-/grunt-run-0.8.1.tgz#502f988947a59f9ad567e2dcb5eeedec712980ec" - integrity sha512-+wvoOJevugcjMLldbVCyspRHHntwVIJiTGjx0HFq+UwXhVPe7AaAiUdY4135CS68pAoRLhd7pAILpL2ITe1tmA== - dependencies: - strip-ansi "^3.0.0" - grunt@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/grunt/-/grunt-1.3.0.tgz#55db6ccd80c6fb53722e496f680620a2e681f809" @@ -16559,13 +16469,6 @@ is-bigint@^1.0.0: resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - 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" @@ -20394,7 +20297,7 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.12.1, nan@^2.13.2, nan@^2.14.0, nan@^2.14.1: +nan@^2.13.2, nan@^2.14.0, nan@^2.14.1: version "2.14.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== @@ -23760,26 +23663,10 @@ readdir-scoped-modules@^1.0.0: graceful-fs "^4.1.2" once "^1.3.0" -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" - integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== - dependencies: - picomatch "^2.0.4" - -readdirp@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" - integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== +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" @@ -28008,11 +27895,6 @@ unzip-response@^1.0.0: resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" integrity sha1-uYTwh3/AqJwsdzzB73tbIytbBv4= -upath@^1.1.0, upath@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" - integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== - update-notifier@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc"